From 835274396f58f9b9f8f11bab8956639337a97a5b Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 27 Oct 2019 13:00:15 +0100 Subject: [PATCH] Feature/netcore3.0 (#433) * Migration to .NET Core 3.0 * Migration to Orleans 3.0 * Nullable support * Better separation of frontend and backend. --- .dockerignore | 16 +- .drone.yml | 16 +- .gitignore | 4 +- Dockerfile | 66 +- Dockerfile.build | 37 - backend/.editorconfig | 16 + NuGet.Config => backend/NuGet.Config | 0 Squidex.ruleset => backend/Squidex.ruleset | 0 Squidex.sln => backend/Squidex.sln | 0 .../Actions/Algolia/AlgoliaAction.cs | 0 .../Actions/Algolia/AlgoliaActionHandler.cs | 135 + .../Actions/Algolia/AlgoliaPlugin.cs | 0 .../Actions/AzureQueue/AzureQueueAction.cs | 0 .../AzureQueue/AzureQueueActionHandler.cs | 0 .../Actions/AzureQueue/AzureQueuePlugin.cs | 0 .../Squidex.Extensions/Actions/ClientPool.cs | 0 .../Actions/Discourse/DiscourseAction.cs | 0 .../Discourse/DiscourseActionHandler.cs | 0 .../Actions/Discourse/DiscoursePlugin.cs | 0 .../ElasticSearch/ElasticSearchAction.cs | 0 .../ElasticSearchActionHandler.cs | 0 .../ElasticSearch/ElasticSearchPlugin.cs | 0 .../Actions/Email/EmailAction.cs | 0 .../Actions/Email/EmailActionHandler.cs | 0 .../Actions/Email/EmailPlugin.cs | 0 .../Actions/Fastly/FastlyAction.cs | 0 .../Actions/Fastly/FastlyActionHandler.cs | 70 + .../Actions/Fastly/FastlyPlugin.cs | 0 .../Squidex.Extensions/Actions/HttpHelper.cs | 0 .../Actions/Kafka/KafkaAction.cs | 0 .../Actions/Kafka/KafkaActionHandler.cs | 0 .../Actions/Kafka/KafkaPlugin.cs | 0 .../Actions/Kafka/KafkaProducer.cs | 0 .../Actions/Kafka/KafkaProducerOptions.cs | 0 .../Actions/Medium/MediumAction.cs | 0 .../Actions/Medium/MediumActionHandler.cs | 0 .../Actions/Medium/MediumPlugin.cs | 0 .../Actions/Prerender/PrerenderAction.cs | 0 .../Prerender/PrerenderActionHandler.cs | 0 .../Actions/Prerender/PrerenderPlugin.cs | 0 .../Actions/Slack/SlackAction.cs | 0 .../Actions/Slack/SlackActionHandler.cs | 68 + .../Actions/Slack/SlackPlugin.cs | 0 .../Actions/Twitter/TweetAction.cs | 0 .../Actions/Twitter/TweetActionHandler.cs | 72 + .../Actions/Twitter/TwitterOptions.cs | 0 .../Actions/Twitter/TwitterPlugin.cs | 0 .../Actions/Webhook/WebhookAction.cs | 0 .../Actions/Webhook/WebhookActionHandler.cs | 86 + .../Actions/Webhook/WebhookPlugin.cs | 0 .../AssetStore/MemoryAssetStorePlugin.cs | 0 .../Samples/Controllers/PluginController.cs | 0 .../Squidex.Extensions.csproj | 31 + .../Apps/AppClient.cs | 46 + .../Apps/AppClients.cs | 90 + .../Apps/AppContributors.cs | 45 + .../Apps/AppImage.cs | 34 + .../Apps/AppPattern.cs | 35 + .../Apps/AppPatterns.cs | 62 + .../Apps/AppPermission.cs | 0 .../Apps/AppPlan.cs | 40 + .../Apps/Json/AppClientsConverter.cs | 0 .../Apps/Json/AppContributorsConverter.cs | 0 .../Apps/Json/AppPatternsConverter.cs | 0 .../Apps/Json/JsonAppClient.cs | 0 .../Apps/Json/JsonAppPattern.cs | 38 + .../Apps/Json/JsonLanguageConfig.cs | 0 .../Apps/Json/JsonLanguagesConfig.cs | 62 + .../Apps/Json/LanguagesConfigConverter.cs | 0 .../Apps/Json/RolesConverter.cs | 0 .../Apps/LanguageConfig.cs | 62 + .../Apps/LanguagesConfig.cs | 179 + .../Apps/Role.cs | 76 + .../Apps/Roles.cs | 180 + .../Comments/Comment.cs | 38 + .../Contents/ContentData.cs | 99 + .../Contents/ContentFieldData.cs | 70 + .../Contents/IdContentData.cs | 55 + .../Json/ContentFieldDataConverter.cs | 65 + .../Contents/Json/JsonWorkflowTransition.cs | 54 + .../Contents/Json/StatusConverter.cs | 0 .../Contents/Json/WorkflowConverter.cs | 0 .../Json/WorkflowTransitionConverter.cs | 0 .../Contents/NamedContentData.cs | 54 + .../Contents/Status.cs | 62 + .../Contents/StatusChange.cs | 0 .../Contents/StatusColors.cs | 0 .../Contents/StatusConverter.cs | 36 + .../Contents/StatusInfo.cs | 0 .../Contents/Workflow.cs | 126 + .../Contents/WorkflowStep.cs | 31 + .../Contents/WorkflowTransition.cs | 27 + .../Contents/Workflows.cs | 83 + .../FodyWeavers.xml | 0 .../FodyWeavers.xsd | 0 .../Freezable.cs | 0 .../IFieldPartitionItem.cs | 0 .../IFieldPartitioning.cs | 0 .../InvariantPartitioning.cs | 74 + .../Squidex.Domain.Apps.Core.Model/Named.cs | 23 + .../Partitioning.cs | 56 + .../PartitioningExtensions.cs | 26 + .../Rules/IRuleTriggerVisitor.cs | 0 .../Rules/Json/JsonRule.cs | 0 .../Rules/Json/RuleConverter.cs | 0 .../Rules/Rule.cs | 116 + .../Rules/RuleAction.cs | 0 .../Rules/RuleJob.cs | 0 .../Rules/RuleTrigger.cs | 0 .../Rules/Triggers/AssetChangedTriggerV2.cs | 0 .../Triggers/ContentChangedTriggerSchemaV2.cs | 18 + .../Rules/Triggers/ContentChangedTriggerV2.cs | 0 .../Rules/Triggers/ManualTrigger.cs | 0 .../Rules/Triggers/SchemaChangedTrigger.cs | 0 .../Rules/Triggers/UsageTrigger.cs | 0 .../Schemas/ArrayField.cs | 91 + .../Schemas/ArrayFieldProperties.cs | 38 + .../Schemas/AssetsFieldProperties.cs | 62 + .../Schemas/BooleanFieldEditor.cs | 0 .../Schemas/BooleanFieldProperties.cs | 38 + .../Schemas/DateTimeCalculatedDefaultValue.cs | 0 .../Schemas/DateTimeFieldEditor.cs | 0 .../Schemas/DateTimeFieldProperties.cs | 44 + .../Schemas/FieldCollection.cs | 171 + .../Schemas/FieldExtensions.cs | 0 .../Schemas/FieldProperties.cs | 34 + .../Schemas/FieldRegistry.cs | 0 .../Schemas/Fields.cs | 236 + .../Schemas/GeolocationFieldEditor.cs | 0 .../Schemas/GeolocationFieldProperties.cs | 34 + .../Schemas/IArrayField.cs | 0 .../Schemas/IField.cs | 0 .../Schemas/IFieldPropertiesVisitor.cs | 0 .../Schemas/IFieldSettings.cs | 0 .../Schemas/IFieldVisitor.cs | 0 .../Schemas/IField{T}.cs | 0 .../Schemas/INestedField.cs | 0 .../Schemas/IRootField.cs | 0 .../Schemas/Json/JsonFieldModel.cs | 57 + .../Schemas/Json/JsonNestedFieldModel.cs | 0 .../Schemas/Json/JsonSchemaModel.cs | 111 + .../Schemas/Json/SchemaConverter.cs | 0 .../Schemas/JsonFieldProperties.cs | 32 + .../Schemas/NamedElementPropertiesBase.cs | 16 + .../Schemas/NestedField.cs | 113 + .../Schemas/NestedField{T}.cs | 68 + .../Schemas/NumberFieldEditor.cs | 0 .../Schemas/NumberFieldProperties.cs | 48 + .../Schemas/ReferencesFieldEditor.cs | 0 .../Schemas/ReferencesFieldProperties.cs | 63 + .../Schemas/RootField.cs | 122 + .../Schemas/RootField{T}.cs | 68 + .../Schemas/Schema.cs | 201 + .../Schemas/SchemaExtensions.cs | 0 .../Schemas/SchemaProperties.cs | 0 .../Schemas/SchemaScripts.cs | 0 .../Schemas/StringFieldEditor.cs | 0 .../Schemas/StringFieldProperties.cs | 52 + .../Schemas/TagsFieldEditor.cs | 0 .../Schemas/TagsFieldNormalization.cs | 0 .../Schemas/TagsFieldProperties.cs | 44 + .../Schemas/UIFieldEditor.cs | 0 .../Schemas/UIFieldProperties.cs | 34 + .../Squidex.Domain.Apps.Core.Model.csproj | 31 + .../SquidexCoreModel.cs | 0 .../ConvertContent/ContentConverter.cs | 160 + .../ConvertContent/ContentConverterFlat.cs | 77 + .../ConvertContent/FieldConverters.cs | 373 + .../ConvertContent/IAssetUrlGenerator.cs | 0 .../ConvertContent/Value.cs | 0 .../ConvertContent/ValueConverters.cs | 0 .../EnrichContent/ContentEnricher.cs | 80 + .../ContentEnrichmentExtensions.cs | 0 .../EnrichContent/DefaultValueFactory.cs | 97 + .../SchemaSynchronizationOptions.cs | 0 .../SchemaSynchronizer.cs | 224 + .../EventSynchronization/SyncHelpers.cs | 0 .../ContentReferencesExtensions.cs | 150 + .../ExtractReferenceIds/Ids.cs | 0 .../ExtractReferenceIds/ReferencesCleaner.cs | 109 + .../ReferencesExtensions.cs | 69 + .../ReferencesExtractor.cs | 116 + .../ValueReferencesConverter.cs | 0 .../GenerateEdmSchema/EdmSchemaExtensions.cs | 69 + .../GenerateEdmSchema/EdmTypeVisitor.cs | 103 + .../GenerateJsonSchema/Builder.cs | 64 + .../ContentSchemaBuilder.cs | 43 + .../JsonSchemaExtensions.cs | 73 + .../GenerateJsonSchema/JsonTypeVisitor.cs | 151 + .../HandleRules/Constants.cs | 0 .../DependencyInjectionExtensions.cs | 0 .../EnrichedEvents/EnrichedAssetEvent.cs | 0 .../EnrichedEvents/EnrichedAssetEventType.cs | 0 .../EnrichedEvents/EnrichedContentEvent.cs | 0 .../EnrichedContentEventType.cs | 0 .../EnrichedEvents/EnrichedEvent.cs | 0 .../EnrichedEvents/EnrichedManualEvent.cs | 0 .../EnrichedEvents/EnrichedSchemaEvent.cs | 0 .../EnrichedEvents/EnrichedSchemaEventBase.cs | 0 .../EnrichedEvents/EnrichedSchemaEventType.cs | 0 .../EnrichedUsageExceededEvent.cs | 0 .../EnrichedEvents/EnrichedUserEventBase.cs | 21 + .../EnrichedEvents/IEnrichedEntityEvent.cs | 0 .../HandleRules/EventEnricher.cs | 78 + .../HandleRules/FormattableAttribute.cs | 0 .../HandleRules/IEventEnricher.cs | 0 .../HandleRules/IRuleActionHandler.cs | 0 .../HandleRules/IRuleTriggerHandler.cs | 27 + .../HandleRules/IRuleUrlGenerator.cs | 0 .../HandleRules/Result.cs | 96 + .../HandleRules/RuleActionAttribute.cs | 0 .../HandleRules/RuleActionDefinition.cs | 0 .../HandleRules/RuleActionHandler.cs | 86 + .../HandleRules/RuleActionProperty.cs | 24 + .../HandleRules/RuleActionPropertyEditor.cs | 0 .../HandleRules/RuleActionRegistration.cs | 24 + .../HandleRules/RuleEventFormatter.cs | 314 + .../HandleRules/RuleOptions.cs | 0 .../HandleRules/RuleRegistry.cs | 189 + .../HandleRules/RuleResult.cs | 0 .../HandleRules/RuleService.cs | 202 + .../HandleRules/RuleTriggerHandler.cs | 63 + .../ContentWrapper/ContentDataObject.cs | 130 + .../ContentWrapper/ContentDataProperty.cs | 65 + .../ContentWrapper/ContentFieldObject.cs | 138 + .../ContentWrapper/ContentFieldProperty.cs | 64 + .../ContentWrapper/CustomProperty.cs | 0 .../Scripting/ContentWrapper/JsonMapper.cs | 131 + .../Scripting/DefaultConverter.cs | 61 + .../Scripting/IScriptEngine.cs | 26 + .../Scripting/JintScriptEngine.cs | 312 + .../Scripting/JintUser.cs | 59 + .../Scripting/NullPropagation.cs | 0 .../Scripting/ScriptContext.cs | 30 + ...Squidex.Domain.Apps.Core.Operations.csproj | 33 + .../SquidexCoreOperations.cs | 0 .../Tags/ITagService.cs | 30 + .../Tags/Tag.cs | 0 .../Tags/TagGroups.cs | 0 .../Tags/TagNormalizer.cs | 150 + .../Tags/TagsExport.cs | 0 .../Tags/TagsSet.cs | 0 .../ContentValidationExtensions.cs | 0 .../ValidateContent/ContentValidator.cs | 108 + .../ValidateContent/Extensions.cs | 0 .../FieldBagValidatorsFactory.cs | 85 + .../FieldValueValidatorsFactory.cs | 191 + .../ValidateContent/IAssetInfo.cs | 0 .../ValidateContent/JsonValueConverter.cs | 231 + .../ValidateContent/ObjectPath.cs | 0 .../ValidateContent/Undefined.cs | 24 + .../ValidateContent/ValidationContext.cs | 127 + .../Validators/AggregateValidator.cs | 33 + .../Validators/AllowedValuesValidator.cs | 42 + .../Validators/AssetsValidator.cs | 116 + .../Validators/CollectionItemValidator.cs | 50 + .../Validators/CollectionValidator.cs | 72 + .../Validators/FieldValidator.cs | 67 + .../ValidateContent/Validators/IValidator.cs | 19 + .../Validators/NoValueValidator.cs | 30 + .../Validators/ObjectValidator.cs | 77 + .../Validators/PatternValidator.cs | 58 + .../Validators/RangeValidator.cs | 62 + .../Validators/ReferencesValidator.cs | 46 + .../Validators/RequiredStringValidator.cs | 42 + .../Validators/RequiredValidator.cs | 25 + .../Validators/StringLengthValidator.cs | 62 + .../Validators/UniqueValidator.cs | 51 + .../Validators/UniqueValuesValidator.cs | 32 + .../Assets/MongoAssetEntity.cs | 0 .../Assets/MongoAssetRepository.cs | 148 + .../MongoAssetRepository_SnapshotStore.cs | 75 + .../Assets/Visitors/FindExtensions.cs | 0 .../Contents/Extensions.cs | 0 .../Contents/MongoContentCollection.cs | 271 + .../Contents/MongoContentEntity.cs | 133 + .../Contents/MongoContentRepository.cs | 156 + .../MongoContentRepository_EventHandling.cs | 0 .../MongoContentRepository_SnapshotStore.cs | 93 + .../Contents/StatusSerializer.cs | 0 .../Contents/Visitors/Adapt.cs | 0 .../Contents/Visitors/AdaptionVisitor.cs | 0 .../Contents/Visitors/FilterFactory.cs | 139 + .../History/MongoHistoryEventRepository.cs | 0 .../Rules/MongoRuleEventEntity.cs | 62 + .../Rules/MongoRuleEventRepository.cs | 136 + .../Rules/MongoRuleStatisticsCollection.cs | 0 ...quidex.Domain.Apps.Entities.MongoDb.csproj | 32 + .../AppProvider.cs | 126 + .../Apps/AppCommandMiddleware.cs | 72 + .../Apps/AppEntityExtensions.cs | 0 .../Apps/AppExtensions.cs | 0 .../Apps/AppGrain.cs | 510 + .../Apps/AppHistoryEventsCreator.cs | 161 + .../Apps/AppUISettings.cs | 67 + .../Apps/AppUISettingsGrain.cs | 115 + .../Apps/BackupApps.cs | 204 + .../Apps/Commands/AddLanguage.cs | 0 .../Apps/Commands/AddPattern.cs | 27 + .../Apps/Commands/AddRole.cs | 0 .../Apps/Commands/AddWorkflow.cs | 0 .../Apps/Commands/AppCommand.cs | 0 .../Apps/Commands/ArchiveApp.cs | 0 .../Apps/Commands/AssignContributor.cs | 0 .../Apps/Commands/AttachClient.cs | 0 .../Apps/Commands/ChangePlan.cs | 0 .../Apps/Commands/CreateApp.cs | 0 .../Apps/Commands/DeletePattern.cs | 0 .../Apps/Commands/DeleteRole.cs | 0 .../Apps/Commands/DeleteWorkflow.cs | 0 .../Apps/Commands/RemoveAppImage.cs | 0 .../Apps/Commands/RemoveContributor.cs | 0 .../Apps/Commands/RemoveLanguage.cs | 0 .../Apps/Commands/RevokeClient.cs | 0 .../Apps/Commands/UpdateApp.cs | 16 + .../Apps/Commands/UpdateClient.cs | 0 .../Apps/Commands/UpdateLanguage.cs | 0 .../Apps/Commands/UpdatePattern.cs | 22 + .../Apps/Commands/UpdateRole.cs | 0 .../Apps/Commands/UpdateWorkflow.cs | 0 .../Apps/Commands/UploadAppImage.cs | 0 .../Apps/DefaultAppLogStore.cs | 34 + .../Diagnostics/OrleansAppsHealthCheck.cs | 36 + .../Apps/Guards/GuardApp.cs | 84 + .../Apps/Guards/GuardAppClients.cs | 104 + .../Apps/Guards/GuardAppContributors.cs | 99 + .../Apps/Guards/GuardAppLanguages.cs | 102 + .../Apps/Guards/GuardAppPatterns.cs | 102 + .../Apps/Guards/GuardAppRoles.cs | 102 + .../Apps/Guards/GuardAppWorkflows.cs | 108 + .../Apps/IAppEntity.cs | 43 + .../Apps/IAppGrain.cs | 0 .../Apps/IAppLogStore.cs | 0 .../Apps/IAppUISettings.cs | 24 + .../Apps/IAppUISettingsGrain.cs | 0 .../Apps/Indexes/AppsByNameIndexGrain.cs | 0 .../Apps/Indexes/AppsByUserIndexGrain.cs | 0 .../Apps/Indexes/AppsIndex.cs | 286 + .../Apps/Indexes/IAppsByNameIndexGrain.cs | 0 .../Apps/Indexes/IAppsByUserIndexGrain.cs | 0 .../Apps/Indexes/IAppsIndex.cs | 39 + .../Apps/InitialPatterns.cs | 0 .../Invitation/InviteUserCommandMiddleware.cs | 52 + .../Apps/Invitation/InvitedResult.cs | 0 .../Apps/RolePermissionsProvider.cs | 76 + .../Apps/Services/IAppLimitsPlan.cs | 28 + .../Apps/Services/IAppPlanBillingManager.cs | 22 + .../Apps/Services/IAppPlansProvider.cs | 28 + .../Apps/Services/IChangePlanResult.cs | 0 .../Implementations/ConfigAppLimitsPlan.cs | 33 + .../Implementations/ConfigAppPlansProvider.cs | 98 + .../NoopAppPlanBillingManager.cs | 31 + .../Apps/Services/PlanChangeAsyncResult.cs | 0 .../Apps/Services/PlanChangedResult.cs | 0 .../Apps/Services/PlanResetResult.cs | 0 .../Apps/Services/RedirectToCheckoutResult.cs | 24 + .../Apps/State/AppState.cs | 252 + .../AlwaysCreateClientCommandMiddleware.cs | 0 .../Templates/Builders/AssetFieldBuilder.cs | 0 .../Templates/Builders/BooleanFieldBuilder.cs | 0 .../Builders/DateTimeFieldBuilder.cs | 0 .../Apps/Templates/Builders/FieldBuilder.cs | 77 + .../Templates/Builders/JsonFieldBuilder.cs | 0 .../Templates/Builders/NumberFieldBuilder.cs | 0 .../Apps/Templates/Builders/SchemaBuilder.cs | 149 + .../Templates/Builders/StringFieldBuilder.cs | 59 + .../Templates/Builders/TagsFieldBuilder.cs | 0 .../Templates/CreateBlogCommandMiddleware.cs | 0 .../CreateIdentityCommandMiddleware.cs | 0 .../CreateProfileCommandMiddleware.cs | 0 .../Apps/Templates/DefaultScripts.cs | 0 .../Assets/AssetChangedTriggerHandler.cs | 69 + .../Assets/AssetCommandMiddleware.cs | 175 + .../Assets/AssetCreatedResult.cs | 0 .../Assets/AssetEntity.cs | 0 .../Assets/AssetExtensions.cs | 0 .../Assets/AssetGrain.cs | 183 + .../Assets/AssetOptions.cs | 0 .../Assets/AssetSlug.cs | 0 .../Assets/AssetStats.cs | 0 .../Assets/AssetUsageTracker.cs | 69 + .../Assets/AssetUsageTracker_EventHandling.cs | 0 .../Assets/BackupAssets.cs | 127 + .../Assets/Commands/AnnotateAsset.cs | 0 .../Assets/Commands/AssetCommand.cs | 0 .../Assets/Commands/CreateAsset.cs | 0 .../Assets/Commands/DeleteAsset.cs | 0 .../Assets/Commands/UpdateAsset.cs | 0 .../Assets/Commands/UploadAssetCommand.cs | 20 + .../Assets/FileTypeTagGenerator.cs | 0 .../Assets/Guards/GuardAsset.cs | 56 + .../Assets/IAssetEnricher.cs | 0 .../Assets/IAssetEntity.cs | 0 .../Assets/IAssetGrain.cs | 0 .../Assets/IAssetLoader.cs | 0 .../Assets/IAssetQueryService.cs | 23 + .../Assets/IAssetUsageTracker.cs | 0 .../Assets/IEnrichedAssetEntity.cs | 0 .../Assets/ImageTagGenerator.cs | 0 .../Assets/Queries/AssetEnricher.cs | 93 + .../Assets/Queries/AssetLoader.cs | 44 + .../Assets/Queries/AssetQueryParser.cs | 174 + .../Assets/Queries/AssetQueryService.cs | 97 + .../Assets/Queries/FilterTagTransformer.cs | 51 + .../Assets/Repositories/IAssetRepository.cs | 30 + .../Assets/State/AssetState.cs | 0 .../Backup/BackupGrain.cs | 262 + .../Backup/BackupHandler.cs | 0 .../Backup/BackupHandlerWithStore.cs | 54 + .../Backup/BackupReader.cs | 155 + .../Backup/BackupRestoreException.cs | 0 .../Backup/BackupVersion.cs | 0 .../Backup/BackupWriter.cs | 108 + .../Backup/GuidMapper.cs | 109 + .../Backup/Helpers/ArchiveHelper.cs | 0 .../Backup/Helpers/Downloader.cs | 87 + .../Backup/Helpers/Safe.cs | 0 .../Backup/IBackupArchiveLocation.cs | 0 .../Backup/IBackupGrain.cs | 0 .../Backup/IBackupJob.cs | 0 .../Backup/IRestoreGrain.cs | 22 + .../Backup/IRestoreJob.cs | 0 .../Backup/JobStatus.cs | 0 .../Backup/Model/CompatibleStoredEvent.cs | 0 .../Backup/RestoreGrain.cs | 367 + .../Backup/State/BackupState.cs | 0 .../Backup/State/BackupStateJob.cs | 0 .../Backup/State/RestoreState.cs | 0 .../Backup/State/RestoreStateJob.cs | 49 + .../Backup/TempFolderBackupArchiveLocation.cs | 0 .../Comments/Commands/CommentsCommand.cs | 0 .../Comments/Commands/CreateComment.cs | 0 .../Comments/Commands/DeleteComment.cs | 0 .../Comments/Commands/UpdateComment.cs | 0 .../Comments/CommentsGrain.cs | 126 + .../Comments/CommentsLoader.cs | 0 .../Comments/CommentsResult.cs | 0 .../Comments/Guards/GuardComments.cs | 89 + .../Comments/ICommentsGrain.cs | 0 .../Comments/ICommentsLoader.cs | 0 .../Comments/State/CommentsState.cs | 0 .../Contents/BackupContents.cs | 0 .../Contents/Commands/ChangeContentStatus.cs | 0 .../Contents/Commands/ContentCommand.cs | 0 .../Contents/Commands/ContentDataCommand.cs | 0 .../Contents/Commands/ContentUpdateCommand.cs | 0 .../Contents/Commands/CreateContent.cs | 0 .../Contents/Commands/DeleteContent.cs | 0 .../Contents/Commands/DiscardChanges.cs | 0 .../Contents/Commands/PatchContent.cs | 0 .../Contents/Commands/UpdateContent.cs | 0 .../Contents/ContentChangedTriggerHandler.cs | 133 + .../Contents/ContentCommandMiddleware.cs | 49 + .../Contents/ContentEntity.cs | 61 + .../Contents/ContentGrain.cs | 377 + .../Contents/ContentHistoryEventsCreator.cs | 74 + .../Contents/ContentOperationContext.cs | 153 + .../Contents/ContentOptions.cs | 0 .../Contents/ContentSchedulerGrain.cs | 107 + .../Contents/ContextExtensions.cs | 0 .../Contents/DefaultContentWorkflow.cs | 0 .../Contents/DefaultWorkflowsValidator.cs | 57 + .../Contents/DynamicContentWorkflow.cs | 153 + .../Contents/GraphQL/CachingGraphQLService.cs | 114 + .../GraphQL/GraphQLExecutionContext.cs | 142 + .../Contents/GraphQL/GraphQLModel.cs | 180 + .../Contents/GraphQL/GraphQLQuery.cs | 0 .../Contents/GraphQL/IGraphModel.cs | 38 + .../Contents/GraphQL/IGraphQLService.cs | 0 .../Contents/GraphQL/IGraphQLUrlGenerator.cs | 26 + .../Contents/GraphQL/Middlewares.cs | 61 + .../Contents/GraphQL/Types/AllTypes.cs | 0 .../GraphQL/Types/AppQueriesGraphType.cs | 0 .../Contents/GraphQL/Types/AssetGraphType.cs | 194 + .../GraphQL/Types/AssetsResultGraphType.cs | 0 .../GraphQL/Types/ContentDataGraphType.cs | 91 + .../GraphQL/Types/ContentGraphType.cs | 144 + .../Types/ContentInterfaceGraphType.cs | 0 .../GraphQL/Types/ContentUnionGraphType.cs | 60 + .../GraphQL/Types/ContentsResultGraphType.cs | 0 .../Contents/GraphQL/Types/Extensions.cs | 0 .../Contents/GraphQL/Types/NestedGraphType.cs | 63 + .../GraphQL/Types/QueryGraphTypeVisitor.cs | 150 + .../GraphQL/Types/Utils/GuidGraphType2.cs | 55 + .../GraphQL/Types/Utils/InstantGraphType.cs | 41 + .../GraphQL/Types/Utils/InstantValue.cs | 0 .../GraphQL/Types/Utils/JsonConverter.cs | 32 + .../GraphQL/Types/Utils/JsonGraphType.cs | 42 + .../GraphQL/Types/Utils/JsonValueNode.cs | 25 + .../GraphQL/Types/Utils/NoopGraphType.cs | 0 .../Contents/Guards/GuardContent.cs | 136 + .../Contents/IContentEnricher.cs | 0 .../Contents/IContentEntity.cs | 35 + .../Contents/IContentGrain.cs | 0 .../Contents/IContentLoader.cs | 0 .../Contents/IContentQueryService.cs | 0 .../Contents/IContentSchedulerGrain.cs | 0 .../Contents/IContentWorkflow.cs | 0 .../Contents/IEnrichedContentEntity.cs | 29 + .../Contents/IWorkflowsValidator.cs | 0 .../Contents/Queries/ContentEnricher.cs | 377 + .../Contents/Queries/ContentLoader.cs | 44 + .../Contents/Queries/ContentQueryParser.cs | 205 + .../Contents/Queries/ContentQueryService.cs | 341 + .../Contents/Queries/FilterTagTransformer.cs | 71 + .../Contents/Queries/QueryExecutionContext.cs | 133 + .../Repositories/IContentRepository.cs | 36 + .../Contents/ScheduleJob.cs | 0 .../Contents/SingletonCommandMiddleware.cs | 0 .../Contents/State/ContentState.cs | 146 + .../Contents/Text/Extensions.cs | 0 .../Contents/Text/GrainTextIndexer.cs | 117 + .../Contents/Text/ITextIndexer.cs | 19 + .../Contents/Text/ITextIndexerGrain.cs | 0 .../Contents/Text/IndexState.cs | 144 + .../Contents/Text/MultiLanguageAnalyzer.cs | 65 + .../Contents/Text/PersistenceHelper.cs | 0 .../Contents/Text/Scope.cs | 0 .../Contents/Text/SearchContext.cs | 0 .../Contents/Text/TextIndexContent.cs | 213 + .../Contents/Text/TextIndexerGrain.cs | 271 + .../Contents/Text/Update.cs | 0 .../Squidex.Domain.Apps.Entities/Context.cs | 71 + .../DomainObjectState.cs | 0 .../EntityExtensions.cs | 0 .../EntityMapper.cs | 82 + .../History/HistoryEvent.cs | 62 + .../History/HistoryEventsCreatorBase.cs | 66 + .../History/HistoryService.cs | 90 + .../History/IHistoryEventsCreator.cs | 20 + .../History/IHistoryService.cs | 0 .../Notifications/INotificationEmailSender.cs | 0 .../NoopNotificationEmailSender.cs | 0 .../NotificationEmailEventConsumer.cs | 121 + .../Notifications/NotificationEmailSender.cs | 113 + .../NotificationEmailTextOptions.cs | 0 .../History/ParsedHistoryEvent.cs | 70 + .../Repositories/IHistoryEventRepository.cs | 0 .../IAppCommand.cs | 0 .../IAppProvider.cs | 36 + .../IContextProvider.cs | 0 .../IEmailUrlGenerator.cs | 0 .../Squidex.Domain.Apps.Entities/IEntity.cs | 0 .../IEntityWithCacheDependencies.cs | 16 + .../IEntityWithCreatedBy.cs | 0 .../IEntityWithLastModifiedBy.cs | 0 .../IEntityWithTags.cs | 0 .../IEntityWithVersion.cs | 0 .../ISchemaCommand.cs | 0 .../IUpdateableEntity.cs | 0 .../IUpdateableEntityWithCreatedBy.cs | 0 .../IUpdateableEntityWithLastModifiedBy.cs | 0 .../IUpdateableEntityWithVersion.cs | 0 backend/src/Squidex.Domain.Apps.Entities/Q.cs | 68 + .../Rules/BackupRules.cs | 54 + .../Rules/Commands/CreateRule.cs | 0 .../Rules/Commands/DeleteRule.cs | 0 .../Rules/Commands/DisableRule.cs | 0 .../Rules/Commands/EnableRule.cs | 0 .../Rules/Commands/RuleCommand.cs | 0 .../Rules/Commands/RuleEditCommand.cs | 0 .../Rules/Commands/TriggerRule.cs | 0 .../Rules/Commands/UpdateRule.cs | 0 .../Rules/Guards/GuardRule.cs | 106 + .../Rules/Guards/RuleTriggerValidator.cs | 104 + .../Rules/IEnrichedRuleEntity.cs | 0 .../Rules/IRuleDequeuerGrain.cs | 0 .../Rules/IRuleEnqueuer.cs | 0 .../Rules/IRuleEnricher.cs | 0 .../Rules/IRuleEntity.cs | 0 .../Rules/IRuleEventEntity.cs | 28 + .../Rules/IRuleGrain.cs | 0 .../Rules/IRuleQueryService.cs | 0 .../Rules/Indexes/IRulesByAppIndexGrain.cs | 0 .../Rules/Indexes/IRulesIndex.cs | 0 .../Rules/Indexes/RulesByAppIndexGrain.cs | 0 .../Rules/Indexes/RulesIndex.cs | 118 + .../Rules/ManualTriggerHandler.cs | 34 + .../Rules/Queries/RuleEnricher.cs | 80 + .../Rules/Queries/RuleQueryService.cs | 0 .../Repositories/IRuleEventRepository.cs | 38 + .../Rules/Repositories/RuleStatistics.cs | 0 .../Rules/RuleCommandMiddleware.cs | 0 .../Rules/RuleDequeuerGrain.cs | 163 + .../Rules/RuleEnqueuer.cs | 102 + .../Rules/RuleEntity.cs | 46 + .../Rules/RuleGrain.cs | 154 + .../Rules/RuleJobResult.cs | 0 .../Rules/State/RuleState.cs | 0 .../Rules/UsageTracking/IUsageTrackerGrain.cs | 0 .../UsageTrackerCommandMiddleware.cs | 61 + .../Rules/UsageTracking/UsageTrackerGrain.cs | 158 + .../UsageTracking/UsageTriggerHandler.cs | 38 + .../Schemas/BackupSchemas.cs | 54 + .../Schemas/Commands/AddField.cs | 0 .../Schemas/Commands/ChangeCategory.cs | 0 .../Schemas/Commands/ConfigurePreviewUrls.cs | 0 .../Schemas/Commands/ConfigureScripts.cs | 0 .../Schemas/Commands/CreateSchema.cs | 0 .../Schemas/Commands/DeleteField.cs | 0 .../Schemas/Commands/DeleteSchema.cs | 0 .../Schemas/Commands/DisableField.cs | 0 .../Schemas/Commands/EnableField.cs | 0 .../Schemas/Commands/FieldCommand.cs | 0 .../Schemas/Commands/HideField.cs | 0 .../Schemas/Commands/LockField.cs | 0 .../Schemas/Commands/ParentFieldCommand.cs | 0 .../Schemas/Commands/PublishSchema.cs | 0 .../Schemas/Commands/ReorderFields.cs | 0 .../Schemas/Commands/SchemaCommand.cs | 0 .../Schemas/Commands/ShowField.cs | 0 .../Schemas/Commands/SynchronizeSchema.cs | 0 .../Schemas/Commands/UnpublishSchema.cs | 0 .../Schemas/Commands/UpdateField.cs | 0 .../Schemas/Commands/UpdateSchema.cs | 0 .../Schemas/Commands/UpsertCommand.cs | 0 .../Schemas/Commands/UpsertSchemaField.cs | 0 .../Schemas/Commands/UpsertSchemaFieldBase.cs | 0 .../Commands/UpsertSchemaNestedField.cs | 0 .../Guards/FieldPropertiesValidator.cs | 0 .../Schemas/Guards/GuardHelper.cs | 0 .../Schemas/Guards/GuardSchema.cs | 251 + .../Schemas/Guards/GuardSchemaField.cs | 167 + .../Schemas/ISchemaEntity.cs | 0 .../Schemas/ISchemaGrain.cs | 0 .../Indexes/ISchemasByAppIndexGrain.cs | 0 .../Schemas/Indexes/ISchemasIndex.cs | 24 + .../Schemas/Indexes/SchemasByAppIndexGrain.cs | 0 .../Schemas/Indexes/SchemasIndex.cs | 181 + .../Schemas/SchemaChangedTriggerHandler.cs | 77 + .../Schemas/SchemaExtensions.cs | 0 .../Schemas/SchemaGrain.cs | 417 + .../Schemas/SchemaHistoryEventsCreator.cs | 93 + .../Schemas/State/SchemaState.cs | 0 .../Squidex.Domain.Apps.Entities.csproj | 41 + .../SquidexCommand.cs | 0 .../SquidexEntities.cs | 0 .../SquidexEventEnricher.cs | 0 .../Tags/GrainTagService.cs | 75 + .../Tags/ITagGenerator.cs | 0 .../Tags/ITagGrain.cs | 31 + .../Tags/TagGrain.cs | 152 + .../Squidex.Domain.Apps.Events/AppEvent.cs | 0 .../AppUsageExceeded.cs | 0 .../Apps/AppArchived.cs | 0 .../Apps/AppClientAttached.cs | 0 .../Apps/AppClientRenamed.cs | 0 .../Apps/AppClientRevoked.cs | 0 .../Apps/AppClientUpdated.cs | 0 .../Apps/AppContributorAssigned.cs | 0 .../Apps/AppContributorRemoved.cs | 0 .../Apps/AppCreated.cs | 0 .../Apps/AppImageRemoved.cs | 0 .../Apps/AppImageUploaded.cs | 0 .../Apps/AppLanguageAdded.cs | 0 .../Apps/AppLanguageRemoved.cs | 0 .../Apps/AppLanguageUpdated.cs | 0 .../Apps/AppMasterLanguageSet.cs | 0 .../Apps/AppPatternAdded.cs | 24 + .../Apps/AppPatternDeleted.cs | 0 .../Apps/AppPatternUpdated.cs | 24 + .../Apps/AppPlanChanged.cs | 0 .../Apps/AppPlanReset.cs | 0 .../Apps/AppRoleAdded.cs | 0 .../Apps/AppRoleDeleted.cs | 0 .../Apps/AppRoleUpdated.cs | 0 .../Apps/AppUpdated.cs | 0 .../Apps/AppWorkflowAdded.cs | 0 .../Apps/AppWorkflowDeleted.cs | 0 .../Apps/AppWorkflowUpdated.cs | 0 .../Assets/AssetAnnotated.cs | 22 + .../Assets/AssetCreated.cs | 36 + .../Assets/AssetDeleted.cs | 0 .../Assets/AssetEvent.cs | 0 .../Assets/AssetUpdated.cs | 0 .../Comments/CommentCreated.cs | 0 .../Comments/CommentDeleted.cs | 0 .../Comments/CommentUpdated.cs | 0 .../Comments/CommentsEvent.cs | 0 .../Contents/ContentChangesDiscarded.cs | 0 .../Contents/ContentChangesPublished.cs | 0 .../Contents/ContentCreated.cs | 0 .../Contents/ContentDeleted.cs | 0 .../Contents/ContentEvent.cs | 0 .../Contents/ContentSchedulingCancelled.cs | 0 .../Contents/ContentStatusChanged.cs | 0 .../Contents/ContentStatusScheduled.cs | 0 .../Contents/ContentUpdateProposed.cs | 0 .../Contents/ContentUpdated.cs | 0 .../Rules/RuleCreated.cs | 0 .../Rules/RuleDeleted.cs | 0 .../Rules/RuleDisabled.cs | 0 .../Rules/RuleEnabled.cs | 0 .../Rules/RuleEvent.cs | 0 .../Rules/RuleManuallyTriggered.cs | 0 .../Rules/RuleUpdated.cs | 0 .../Squidex.Domain.Apps.Events/SchemaEvent.cs | 0 .../Schemas/FieldAdded.cs | 22 + .../Schemas/FieldDeleted.cs | 0 .../Schemas/FieldDisabled.cs | 0 .../Schemas/FieldEnabled.cs | 0 .../Schemas/FieldEvent.cs | 0 .../Schemas/FieldHidden.cs | 0 .../Schemas/FieldLocked.cs | 0 .../Schemas/FieldShown.cs | 0 .../Schemas/FieldUpdated.cs | 0 .../Schemas/ParentFieldEvent.cs | 16 + .../Schemas/SchemaCategoryChanged.cs | 0 .../Schemas/SchemaCreated.cs | 0 .../Schemas/SchemaCreatedField.cs | 0 .../Schemas/SchemaCreatedFieldBase.cs | 0 .../Schemas/SchemaCreatedNestedField.cs | 0 .../Schemas/SchemaDeleted.cs | 0 .../Schemas/SchemaFieldsReordered.cs | 0 .../Schemas/SchemaPreviewUrlsConfigured.cs | 0 .../Schemas/SchemaPublished.cs | 0 .../Schemas/SchemaScriptsConfigured.cs | 0 .../Schemas/SchemaUnpublished.cs | 0 .../Schemas/SchemaUpdated.cs | 0 .../Squidex.Domain.Apps.Events.csproj | 29 + .../SquidexEvent.cs | 0 .../SquidexEvents.cs | 0 .../SquidexHeaderExtensions.cs | 0 .../SquidexHeaders.cs | 0 .../MongoPersistedGrantStore.cs | 0 .../MongoRoleStore.cs | 0 .../Squidex.Domain.Users.MongoDb/MongoUser.cs | 99 + .../MongoUserStore.cs | 526 + .../Squidex.Domain.Users.MongoDb.csproj | 32 + .../AssetUserPictureStore.cs | 42 + .../DefaultUserResolver.cs | 101 + .../DefaultXmlRepository.cs | 53 + .../src}/Squidex.Domain.Users/IUserEvents.cs | 0 .../src}/Squidex.Domain.Users/IUserFactory.cs | 0 .../Squidex.Domain.Users/IUserPictureStore.cs | 0 .../Squidex.Domain.Users/NoopUserEvents.cs | 0 .../PwnedPasswordValidator.cs | 54 + .../Squidex.Domain.Users.csproj | 31 + .../UserClaimsPrincipalFactoryWithEmail.cs | 0 .../UserManagerExtensions.cs | 286 + .../src}/Squidex.Domain.Users/UserValues.cs | 0 .../Squidex.Domain.Users/UserWithClaims.cs | 54 + .../Assets/AzureBlobAssetStore.cs | 142 + .../Diagnostics/CosmosDbHealthCheck.cs | 0 .../EventSourcing/Constants.cs | 0 .../EventSourcing/CosmosDbEvent.cs | 0 .../EventSourcing/CosmosDbEventCommit.cs | 0 .../EventSourcing/CosmosDbEventStore.cs | 139 + .../CosmosDbEventStore_Reader.cs | 142 + .../CosmosDbEventStore_Writer.cs | 149 + .../EventSourcing/CosmosDbSubscription.cs | 151 + .../EventSourcing/FilterBuilder.cs | 156 + .../EventSourcing/FilterExtensions.cs | 62 + .../EventSourcing/StreamPosition.cs | 55 + .../Squidex.Infrastructure.Azure.csproj | 24 + .../Diagnostics/GetEventStoreHealthCheck.cs | 33 + .../EventSourcing/Formatter.cs | 78 + .../EventSourcing/GetEventStore.cs | 224 + .../GetEventStoreSubscription.cs | 81 + .../EventSourcing/ProjectionClient.cs | 142 + ...quidex.Infrastructure.GetEventStore.csproj | 26 + .../Assets/GoogleCloudAssetStore.cs | 112 + .../Squidex.Infrastructure.GoogleCloud.csproj | 27 + .../Assets/MongoGridFsAssetStore.cs | 131 + .../Diagnostics/MongoDBHealthCheck.cs | 39 + .../EventSourcing/MongoEvent.cs | 0 .../EventSourcing/MongoEventCommit.cs | 0 .../EventSourcing/MongoEventStore.cs | 66 + .../EventSourcing/MongoEventStore_Reader.cs | 210 + .../EventSourcing/MongoEventStore_Writer.cs | 144 + .../EventSourcing/StreamPosition.cs | 60 + .../Migrations/MongoMigrationEntity.cs | 0 .../Migrations/MongoMigrationStatus.cs | 0 .../MongoDb/Batching.cs | 0 .../MongoDb/BsonHelper.cs | 0 .../MongoDb/BsonJsonAttribute.cs | 0 .../MongoDb/BsonJsonConvention.cs | 58 + .../MongoDb/BsonJsonReader.cs | 107 + .../MongoDb/BsonJsonSerializer.cs | 60 + .../MongoDb/BsonJsonWriter.cs | 178 + .../MongoDb/FieldDefinitionBuilder.cs | 0 .../MongoDb/IVersionedEntity.cs | 0 .../MongoDb/InstantSerializer.cs | 0 .../MongoDb/JTokenSerializer.cs | 53 + .../MongoDb/MongoEntity.cs | 0 .../MongoDb/MongoExtensions.cs | 216 + .../MongoDb/MongoRepositoryBase.cs | 101 + .../MongoDb/Queries/FilterBuilder.cs | 41 + .../MongoDb/Queries/FilterVisitor.cs | 92 + .../MongoDb/Queries/LimitExtensions.cs | 0 .../MongoDb/Queries/SortBuilder.cs | 54 + .../MongoDb/RefTokenSerializer.cs | 0 .../Squidex.Infrastructure.MongoDb.csproj | 29 + .../States/MongoSnapshotStore.cs | 80 + .../States/MongoState.cs | 0 .../UsageTracking/MongoUsage.cs | 0 .../UsageTracking/MongoUsageRepository.cs | 105 + .../CQRS/Events/RabbitMqEventConsumer.cs | 104 + .../Squidex.Infrastructure.RabbitMq.csproj | 27 + .../RedisPubSub.cs | 0 .../RedisSubscription.cs | 0 .../Squidex.Infrastructure.Redis.csproj | 26 + .../Assets/AssetAlreadyExistsException.cs | 38 + .../Assets/AssetFile.cs | 42 + .../Assets/AssetNotFoundException.cs | 38 + .../Assets/AssetStoreExtensions.cs | 74 + .../Assets/FTPAssetStore.cs | 158 + .../Assets/FolderAssetStore.cs | 142 + .../Assets/HasherStream.cs | 96 + .../Assets/IAssetStore.cs | 26 + .../Assets/IAssetThumbnailGenerator.cs | 19 + .../Assets/ImageInfo.cs | 25 + .../ImageSharpAssetThumbnailGenerator.cs | 95 + .../Assets/MemoryAssetStore.cs | 113 + .../Assets/NoopAssetStore.cs | 42 + .../Caching/AsyncLocalCache.cs | 79 + .../Caching/CachingProviderBase.cs | 28 + .../Caching/ILocalCache.cs | 22 + .../Caching/LRUCache.cs | 106 + .../Caching/LRUCacheItem.cs | 19 + .../Caching/RequestCacheExtensions.cs | 0 .../src}/Squidex.Infrastructure/Cloneable.cs | 0 .../Squidex.Infrastructure/Cloneable{T}.cs | 0 .../CollectionExtensions.cs | 234 + .../Collections/ArrayDictionary.cs | 21 + .../ArrayDictionary{TKey,TValue}.cs | 165 + .../Collections/ReadOnlyCollection.cs | 0 .../Commands/CommandContext.cs | 53 + .../Commands/CommandExtensions.cs | 0 .../Commands/CustomCommandMiddlewareRunner.cs | 41 + .../Commands/DomainObjectGrain.cs | 74 + .../Commands/DomainObjectGrainBase.cs | 226 + .../Commands/DomainObjectGrainFormatter.cs | 40 + .../EnrichWithTimestampCommandMiddleware.cs | 35 + .../Commands/EntityCreatedResult.cs | 0 .../Commands/EntityCreatedResult{T}.cs | 0 .../Commands/EntitySavedResult.cs | 0 .../Commands/GrainCommandMiddleware.cs | 51 + .../Commands/IAggregateCommand.cs | 0 .../Commands/ICommand.cs | 0 .../Commands/ICommandBus.cs | 0 .../Commands/ICommandMiddleware.cs | 0 .../Commands/ICustomCommandMiddleware.cs | 0 .../Commands/IDomainObjectGrain.cs | 18 + .../Commands/IDomainState.cs | 0 .../Commands/ITimestampCommand.cs | 0 .../Commands/InMemoryCommandBus.cs | 50 + .../Commands/LogCommandMiddleware.cs | 73 + .../Commands/LogSnapshotDomainObjectGrain.cs | 96 + .../Commands/ReadonlyCommandMiddleware.cs | 35 + .../Commands/ReadonlyOptions.cs | 0 .../Configuration/Alternatives.cs | 0 .../Configuration/ConfigurationExtensions.cs | 0 .../ConfigurationException.cs | 0 .../DelegateDisposable.cs | 28 + .../DependencyInjectionExtensions.cs | 96 + .../Diagnostics/GCHealthCheck.cs | 47 + .../Diagnostics/GCHealthCheckOptions.cs | 0 .../Diagnostics/OrleansHealthCheck.cs | 38 + .../DisposableObjectBase.cs | 0 .../Squidex.Infrastructure/DomainException.cs | 31 + .../DomainForbiddenException.cs | 0 .../DomainObjectDeletedException.cs | 0 .../DomainObjectException.cs | 44 + .../DomainObjectNotFoundException.cs | 0 .../DomainObjectVersionException.cs | 0 .../Email/IEmailSender.cs | 0 .../Email/SmptOptions.cs | 0 .../Email/SmtpEmailSender.cs | 42 + .../Squidex.Infrastructure/EtagVersion.cs | 0 .../EventSourcing/CommonHeaders.cs | 0 .../EventSourcing/CompoundEventConsumer.cs | 77 + .../DefaultEventDataFormatter.cs | 73 + .../EventSourcing/DefaultEventEnricher.cs | 0 .../EventSourcing/Envelope.cs | 0 .../EventSourcing/EnvelopeExtensions.cs | 0 .../EventSourcing/EnvelopeHeaders.cs | 0 .../EventSourcing/Envelope{T}.cs | 44 + .../EventSourcing/EventConsumerInfo.cs | 0 .../EventSourcing/EventData.cs | 31 + .../EventSourcing/EventTypeAttribute.cs | 0 .../Grains/EventConsumerGrain.cs | 305 + .../Grains/EventConsumerManagerGrain.cs | 118 + .../Grains/EventConsumerState.cs | 51 + .../Grains/IEventConsumerGrain.cs | 0 .../Grains/IEventConsumerManagerGrain.cs | 0 .../Grains/OrleansEventNotifier.cs | 33 + .../Grains/WrapperSubscription.cs | 0 .../EventSourcing/IEvent.cs | 0 .../EventSourcing/IEventConsumer.cs | 0 .../EventSourcing/IEventDataFormatter.cs | 18 + .../EventSourcing/IEventEnricher.cs | 0 .../EventSourcing/IEventNotifier.cs | 0 .../EventSourcing/IEventStore.cs | 33 + .../EventSourcing/IEventSubscriber.cs | 0 .../EventSourcing/IEventSubscription.cs | 0 .../EventSourcing/NoopEvent.cs | 0 .../EventSourcing/PollingSubscription.cs | 58 + .../EventSourcing/RetrySubscription.cs | 117 + .../EventSourcing/StoredEvent.cs | 34 + .../EventSourcing/StreamFilter.cs | 23 + .../WrongEventVersionException.cs | 0 .../Squidex.Infrastructure/ExceptionHelper.cs | 0 .../Squidex.Infrastructure/FileExtensions.cs | 0 .../Squidex.Infrastructure/GravatarHelper.cs | 0 backend/src/Squidex.Infrastructure/Guard.cs | 220 + .../src}/Squidex.Infrastructure/HashSet.cs | 0 .../Http/DumpFormatter.cs | 107 + .../IBackgroundProcess.cs | 0 .../src}/Squidex.Infrastructure/IFreezable.cs | 0 .../Squidex.Infrastructure/IInitializable.cs | 0 .../Squidex.Infrastructure/IResultList.cs | 0 .../InstantExtensions.cs | 0 .../Json/IJsonSerializer.cs | 23 + .../Newtonsoft/ClaimsPrincipalConverter.cs | 0 .../Newtonsoft/ConverterContractResolver.cs | 100 + .../Newtonsoft/EnvelopeHeadersConverter.cs | 0 .../Json/Newtonsoft/ISupportedTypes.cs | 0 .../Json/Newtonsoft/InstantConverter.cs | 64 + .../Json/Newtonsoft/JsonClassConverter.cs | 51 + .../Json/Newtonsoft/JsonValueConverter.cs | 184 + .../Json/Newtonsoft/LanguageConverter.cs | 0 .../Json/Newtonsoft/NamedGuidIdConverter.cs | 0 .../Json/Newtonsoft/NamedLongIdConverter.cs | 0 .../Json/Newtonsoft/NamedStringIdConverter.cs | 0 .../Newtonsoft/NewtonsoftJsonSerializer.cs | 100 + .../Json/Newtonsoft/RefTokenConverter.cs | 0 .../Newtonsoft/TypeNameSerializationBinder.cs | 48 + .../Json/Objects/IJsonValue.cs | 20 + .../Json/Objects/JsonArray.cs | 96 + .../Json/Objects/JsonBoolean.cs | 0 .../Json/Objects/JsonNull.cs | 55 + .../Json/Objects/JsonNumber.cs | 0 .../Json/Objects/JsonObject.cs | 136 + .../Json/Objects/JsonScalar.cs | 53 + .../Json/Objects/JsonString.cs | 0 .../Json/Objects/JsonValue.cs | 136 + .../Json/Objects/JsonValueType.cs | 0 .../src/Squidex.Infrastructure/Language.cs | 113 + .../src}/Squidex.Infrastructure/Languages.cs | 0 .../LanguagesInitializer.cs | 39 + .../LanguagesOptions.cs | 0 .../Log/Adapter/SemanticLogLogger.cs | 0 .../SemanticLogLoggerFactoryExtensions.cs | 0 .../Log/Adapter/SemanticLogLoggerProvider.cs | 58 + .../Log/ApplicationInfoLogAppender.cs | 41 + .../Log/ConsoleLogChannel.cs | 0 .../Log/ConstantsLogWriter.cs | 28 + .../Log/DebugLogChannel.cs | 0 .../Squidex.Infrastructure/Log/FileChannel.cs | 51 + .../Log/IArrayWriter.cs | 0 .../Log/ILogAppender.cs | 0 .../Squidex.Infrastructure/Log/ILogChannel.cs | 0 .../Squidex.Infrastructure/Log/ILogStore.cs | 0 .../Log/IObjectWriter.cs | 37 + .../Log/IObjectWriterFactory.cs | 0 .../Log/ISemanticLog.cs | 0 .../Log/Internal/AnsiLogConsole.cs | 0 .../Log/Internal/ConsoleLogProcessor.cs | 108 + .../Log/Internal/FileLogProcessor.cs | 0 .../Log/Internal/IConsole.cs | 0 .../Log/Internal/LogMessageEntry.cs | 0 .../Log/Internal/WindowsLogConsole.cs | 0 .../Log/JsonLogWriter.cs | 225 + .../Log/JsonLogWriterFactory.cs | 0 .../Log/LockingLogStore.cs | 83 + .../Log/NoopDisposable.cs | 0 .../Log/NoopLogStore.cs | 0 .../Squidex.Infrastructure/Log/Profiler.cs | 74 + .../Log/ProfilerSession.cs | 58 + .../Log/ProfilerSpan.cs | 66 + .../Squidex.Infrastructure/Log/SemanticLog.cs | 98 + .../Log/SemanticLogExtensions.cs | 194 + .../Log/SemanticLogLevel.cs | 0 .../Log/TimestampLogAppender.cs | 26 + .../Migrations/IMigrated.cs | 0 .../Migrations/IMigration.cs | 0 .../Migrations/IMigrationPath.cs | 16 + .../Migrations/IMigrationStatus.cs | 0 .../Migrations/MigrationFailedException.cs | 46 + .../Migrations/Migrator.cs | 101 + backend/src/Squidex.Infrastructure/NamedId.cs | 17 + .../src/Squidex.Infrastructure/NamedId{T}.cs | 102 + .../Net/IPAddressComparer.cs | 0 backend/src/Squidex.Infrastructure/None.cs | 22 + .../Orleans/ActivationLimit.cs | 77 + .../Orleans/ActivationLimiter.cs | 0 .../Orleans/ActivationLimiterFilter.cs | 0 .../Orleans/GrainBase.cs | 63 + .../Orleans/GrainBootstrap.cs | 55 + .../Orleans/GrainOfGuid.cs | 0 .../Orleans/GrainOfString.cs | 0 .../Orleans/GrainState.cs | 85 + .../Orleans/IActivationLimit.cs | 0 .../Orleans/IActivationLimiter.cs | 0 .../Orleans/IBackgroundGrain.cs | 0 .../Orleans/IDeactivater.cs | 0 .../Orleans/IGrainState.cs | 0 .../Orleans/ILockGrain.cs | 19 + .../Orleans/Indexes/IIdsIndexGrain.cs | 0 .../Orleans/Indexes/IUniqueNameIndexGrain.cs | 35 + .../Orleans/Indexes/IdsIndexGrain.cs | 62 + .../Orleans/Indexes/IdsIndexState.cs | 0 .../Orleans/Indexes/UniqueNameIndexGrain.cs | 138 + .../Orleans/Indexes/UniqueNameIndexState.cs | 0 .../src}/Squidex.Infrastructure/Orleans/J.cs | 0 .../Squidex.Infrastructure/Orleans/J{T}.cs | 95 + .../Orleans/LocalCacheFilter.cs | 41 + .../Orleans/LockGrain.cs | 46 + .../Orleans/LoggingFilter.cs | 48 + .../Orleans/SingleGrain.cs | 0 .../Orleans/StateFilter.cs | 0 .../Orleans/StreamReaderWrapper.cs | 88 + .../Orleans/StreamWriterWrapper.cs | 0 .../Squidex.Infrastructure/Plugins/IPlugin.cs | 0 .../Plugins/IWebPlugin.cs | 0 .../Plugins/PluginManager.cs | 124 + .../Plugins/PluginOptions.cs | 0 .../Queries/ClrFilter.cs | 87 + .../Queries/ClrQuery.cs | 0 .../Queries/ClrValue.cs | 140 + .../Queries/ClrValueType.cs | 0 .../Queries/CompareFilter.cs | 67 + .../Queries/CompareOperator.cs | 0 .../Queries/FilterNode.cs | 16 + .../Queries/FilterNodeVisitor.cs | 0 .../Queries/Json/FilterConverter.cs | 165 + .../Queries/Json/JsonFilterVisitor.cs | 84 + .../Queries/Json/OperatorValidator.cs | 0 .../Queries/Json/PropertyPathConverter.cs | 0 .../Queries/Json/PropertyPathValidator.cs | 48 + .../Queries/Json/QueryParser.cs | 76 + .../Queries/Json/ValueConverter.cs | 238 + .../Queries/LogicalFilter.cs | 38 + .../Queries/LogicalFilterType.cs | 0 .../Queries/NegateFilter.cs | 31 + .../Queries/OData/ConstantVisitor.cs | 0 .../Queries/OData/ConstantWithTypeVisitor.cs | 178 + .../Queries/OData/EdmModelExtensions.cs | 61 + .../Queries/OData/FilterBuilder.cs | 0 .../Queries/OData/FilterVisitor.cs | 0 .../Queries/OData/LimitExtensions.cs | 0 .../Queries/OData/PropertyPathVisitor.cs | 0 .../Queries/OData/SearchTermVisitor.cs | 0 .../Queries/OData/SortBuilder.cs | 0 .../Queries/Optimizer.cs | 67 + .../Queries/PascalCasePathConverter.cs | 30 + .../Queries/PropertyPath.cs | 54 + .../Squidex.Infrastructure/Queries/Query.cs | 56 + .../Queries/SortBuilder.cs | 0 .../Queries/SortNode.cs | 33 + .../Queries/SortOrder.cs | 0 .../Queries/TransformVisitor.cs | 38 + .../src}/Squidex.Infrastructure/RandomHash.cs | 0 .../src/Squidex.Infrastructure/RefToken.cs | 88 + .../Squidex.Infrastructure/RefTokenType.cs | 0 .../Reflection/AutoAssembyTypeProvider.cs | 0 .../Reflection/IPropertyAccessor.cs | 16 + .../Reflection/ITypeProvider.cs | 0 .../Reflection/PropertiesTypeAccessor.cs | 78 + .../Reflection/PropertyAccessor.cs | 76 + .../Reflection/ReflectionExtensions.cs | 0 .../Reflection/SimpleCopier.cs | 84 + .../Reflection/SimpleMapper.cs | 186 + .../Reflection/TypeNameAttribute.cs | 0 .../Reflection/TypeNameBuilder.cs | 0 .../Reflection/TypeNameNotFoundException.cs | 0 .../Reflection/TypeNameRegistry.cs | 163 + .../src}/Squidex.Infrastructure/ResultList.cs | 0 .../src/Squidex.Infrastructure/RetryWindow.cs | 48 + .../Security/Extensions.cs | 75 + .../Security/OpenIdClaims.cs | 0 .../Security/Permission.Part.cs | 84 + .../Security/Permission.cs | 118 + .../Security/PermissionSet.cs | 91 + .../src}/Squidex.Infrastructure/Singletons.cs | 0 .../Squidex.Infrastructure.csproj | 46 + .../SquidexInfrastructure.cs | 0 .../States/CollectionNameAttribute.cs | 0 .../States/DefaultStreamNameResolver.cs | 45 + .../States/IPersistence.cs | 0 .../States/IPersistence{TState}.cs | 0 .../States/ISnapshotStore.cs | 0 .../Squidex.Infrastructure/States/IStore.cs | 27 + .../States/IStreamNameResolver.cs | 18 + .../States/InconsistentStateException.cs | 58 + .../States/Persistence.cs | 26 + .../States/PersistenceMode.cs | 0 .../States/Persistence{TSnapshot,TKey}.cs | 241 + .../Squidex.Infrastructure/States/Store.cs | 73 + .../States/StoreExtensions.cs | 0 .../StringExtensions.cs | 801 + .../Tasks/AsyncLocalCleaner.cs | 29 + .../Squidex.Infrastructure/Tasks/AsyncLock.cs | 73 + .../Tasks/AsyncLockPool.cs | 36 + .../Tasks/PartitionedActionBlock.cs | 98 + .../Tasks/SingleThreadedDispatcher.cs | 67 + .../Tasks/TaskExtensions.cs | 103 + .../Tasks/TaskHelper.cs | 36 + .../Timers/CompletionTimer.cs | 89 + .../Translations/DeepLTranslator.cs | 97 + .../Translations/DeepLTranslatorOptions.cs | 14 + .../Translations/ITranslator.cs | 17 + .../Translations/NoopTranslator.cs | 22 + .../Translations/Translation.cs | 25 + .../Translations/TranslationResult.cs | 0 .../UsageTracking/BackgroundUsageTracker.cs | 198 + .../UsageTracking/CachingUsageTracker.cs | 71 + .../UsageTracking/Counters.cs | 0 .../UsageTracking/DateUsage.cs | 0 .../UsageTracking/IUsageRepository.cs | 0 .../UsageTracking/IUsageTracker.cs | 24 + .../UsageTracking/StoredUsage.cs | 30 + .../UsageTracking/Usage.cs | 0 .../UsageTracking/UsageUpdate.cs | 0 .../Validation/AbsoluteUrlAttribute.cs | 0 .../Validation/IValidatable.cs | 0 .../Squidex.Infrastructure/Validation/Not.cs | 0 .../Validation/Validate.cs | 62 + .../Validation/ValidationError.cs | 56 + .../Validation/ValidationException.cs | 106 + .../Validation/ValidationExtensions.cs | 0 .../Squidex.Infrastructure/ValueStopwatch.cs | 0 .../Squidex.Infrastructure/language-codes.csv | 0 .../src}/Squidex.Shared/DefaultClients.cs | 0 .../Identity/ClaimsPrincipalExtensions.cs | 0 .../Identity/SquidexClaimTypes.cs | 0 backend/src/Squidex.Shared/Permissions.cs | 184 + .../src/Squidex.Shared/Squidex.Shared.csproj | 25 + .../src/Squidex.Shared/Users/ClientUser.cs | 54 + .../src}/Squidex.Shared/Users/IUser.cs | 0 .../src/Squidex.Shared/Users/IUserResolver.cs | 23 + .../Squidex.Shared/Users/UserExtensions.cs | 113 + backend/src/Squidex.Web/ApiController.cs | 68 + .../src}/Squidex.Web/ApiCostsAttribute.cs | 0 .../ApiExceptionFilterAttribute.cs | 117 + .../ApiModelValidationAttribute.cs | 0 .../Squidex.Web/ApiPermissionAttribute.cs | 0 .../AssetRequestSizeLimitAttribute.cs | 40 + .../src}/Squidex.Web/ClearCookiesAttribute.cs | 0 .../ETagCommandMiddleware.cs | 0 .../EnrichWithActorCommandMiddleware.cs | 0 .../EnrichWithAppIdCommandMiddleware.cs | 0 .../EnrichWithSchemaIdCommandMiddleware.cs | 102 + {src => backend/src}/Squidex.Web/Constants.cs | 0 .../src}/Squidex.Web/ContextExtensions.cs | 0 backend/src/Squidex.Web/ContextProvider.cs | 45 + backend/src/Squidex.Web/Deferred.cs | 42 + .../src}/Squidex.Web/ETagExtensions.cs | 0 backend/src/Squidex.Web/EntityCreatedDto.cs | 27 + {src => backend/src}/Squidex.Web/ErrorDto.cs | 0 .../src}/Squidex.Web/ExposedConfiguration.cs | 0 backend/src/Squidex.Web/ExposedValues.cs | 65 + backend/src/Squidex.Web/Extensions.cs | 67 + backend/src/Squidex.Web/FileCallbackResult.cs | 42 + .../src}/Squidex.Web/FileExtensions.cs | 0 .../src}/Squidex.Web/IApiCostsFeature.cs | 0 .../Json/TypedJsonInheritanceConverter.cs | 95 + .../src/Squidex.Web/PermissionExtensions.cs | 64 + .../Pipeline/ActionContextLogAppender.cs | 0 .../Squidex.Web/Pipeline/ApiCostsFilter.cs | 88 + .../Pipeline/ApiPermissionUnifier.cs | 0 .../src/Squidex.Web/Pipeline/AppResolver.cs | 113 + .../Pipeline/CleanupHostMiddleware.cs | 0 .../Pipeline/DeferredActionFilter.cs | 0 .../src}/Squidex.Web/Pipeline/ETagFilter.cs | 0 .../src}/Squidex.Web/Pipeline/ETagOptions.cs | 0 .../Pipeline/EnforceHttpsMiddleware.cs | 0 .../Pipeline/FileCallbackResultExecutor.cs | 0 .../Pipeline/LocalCacheMiddleware.cs | 34 + .../Pipeline/MeasureResultFilter.cs | 0 .../RequestLogPerformanceMiddleware.cs | 0 backend/src/Squidex.Web/Resource.cs | 61 + backend/src/Squidex.Web/ResourceLink.cs | 25 + .../src/Squidex.Web/Services/UrlGenerator.cs | 78 + backend/src/Squidex.Web/Squidex.Web.csproj | 29 + .../src}/Squidex.Web/SquidexWeb.cs | 0 .../src/Squidex.Web/UrlHelperExtensions.cs | 46 + .../src}/Squidex.Web/UrlsOptions.cs | 0 .../src}/Squidex.Web/UsageOptions.cs | 0 .../Api/Config/OpenApi/CommonProcessor.cs | 50 + .../Api/Config/OpenApi/ErrorDtoProcessor.cs | 0 .../Areas/Api/Config/OpenApi/FixProcessor.cs | 0 .../Api/Config/OpenApi/ODataExtensions.cs | 0 .../OpenApi/ODataQueryParamsProcessor.cs | 0 .../Api/Config/OpenApi/OpenApiExtensions.cs | 19 + .../Api/Config/OpenApi/OpenApiServices.cs | 85 + .../Api/Config/OpenApi/ScopesProcessor.cs | 58 + .../Api/Config/OpenApi/SecurityProcessor.cs | 0 .../Config/OpenApi/TagByGroupNameProcessor.cs | 0 .../OpenApi/XmlResponseTypesProcessor.cs | 55 + .../Api/Config/OpenApi/XmlTagProcessor.cs | 47 + .../Controllers/Apps/AppClientsController.cs | 0 .../Apps/AppContributorsController.cs | 0 .../Apps/AppLanguagesController.cs | 0 .../Controllers/Apps/AppPatternsController.cs | 0 .../Controllers/Apps/AppRolesController.cs | 0 .../Apps/AppWorkflowsController.cs | 0 .../Api/Controllers/Apps/AppsController.cs | 302 + .../Controllers/Apps/Models/AddLanguageDto.cs | 0 .../Api/Controllers/Apps/Models/AddRoleDto.cs | 0 .../Controllers/Apps/Models/AddWorkflowDto.cs | 0 .../Api/Controllers/Apps/Models/AppDto.cs | 244 + .../Controllers/Apps/Models/AppLanguageDto.cs | 0 .../Apps/Models/AppLanguagesDto.cs | 0 .../Apps/Models/AssignContributorDto.cs | 0 .../Api/Controllers/Apps/Models/ClientDto.cs | 0 .../Api/Controllers/Apps/Models/ClientsDto.cs | 0 .../Controllers/Apps/Models/ContributorDto.cs | 76 + .../Apps/Models/ContributorsDto.cs | 0 .../Apps/Models/ContributorsMetadata.cs | 0 .../Controllers/Apps/Models/CreateAppDto.cs | 0 .../Apps/Models/CreateClientDto.cs | 0 .../Api/Controllers/Apps/Models/PatternDto.cs | 0 .../Controllers/Apps/Models/PatternsDto.cs | 0 .../Api/Controllers/Apps/Models/RoleDto.cs | 0 .../Api/Controllers/Apps/Models/RolesDto.cs | 0 .../Controllers/Apps/Models/UpdateAppDto.cs | 0 .../Apps/Models/UpdateClientDto.cs | 0 .../Apps/Models/UpdateLanguageDto.cs | 0 .../Apps/Models/UpdatePatternDto.cs | 0 .../Controllers/Apps/Models/UpdateRoleDto.cs | 0 .../Apps/Models/UpdateWorkflowDto.cs | 53 + .../Controllers/Apps/Models/WorkflowDto.cs | 77 + .../Apps/Models/WorkflowStepDto.cs | 58 + .../Apps/Models/WorkflowTransitionDto.cs | 40 + .../Controllers/Apps/Models/WorkflowsDto.cs | 0 .../Assets/AssetContentController.cs | 202 + .../Controllers/Assets/AssetsController.cs | 319 + .../Assets/Models/AnnotateAssetDto.cs | 0 .../Api/Controllers/Assets/Models/AssetDto.cs | 0 .../Assets/Models/AssetMetadata.cs | 0 .../Controllers/Assets/Models/AssetQuery.cs | 0 .../Controllers/Assets/Models/AssetsDto.cs | 0 .../Backups/BackupContentController.cs | 0 .../Controllers/Backups/BackupsController.cs | 0 .../Backups/Models/BackupJobDto.cs | 0 .../Backups/Models/BackupJobsDto.cs | 0 .../Backups/Models/RestoreJobDto.cs | 0 .../Backups/Models/RestoreRequestDto.cs | 0 .../Controllers/Backups/RestoreController.cs | 80 + .../Comments/CommentsController.cs | 0 .../Controllers/Comments/Models/CommentDto.cs | 0 .../Comments/Models/CommentsDto.cs | 0 .../Comments/Models/UpsertCommentDto.cs | 0 .../Contents/ContentOpenApiController.cs | 0 .../Contents/ContentsController.cs | 457 + .../Generator/SchemaOpenApiGenerator.cs | 0 .../Generator/SchemasOpenApiGenerator.cs | 0 .../Contents/Models/ChangeStatusDto.cs | 0 .../Controllers/Contents/Models/ContentDto.cs | 194 + .../Contents/Models/ContentsDto.cs | 81 + .../Contents/Models/ScheduleJobDto.cs | 0 .../Contents/Models/StatusInfoDto.cs | 0 .../Contents/MyContentsControllerOptions.cs | 0 .../Api/Controllers/Docs/DocsController.cs | 0 .../Squidex/Areas/Api/Controllers/DocsVM.cs | 0 .../EventConsumersController.cs | 0 .../EventConsumers/Models/EventConsumerDto.cs | 0 .../Models/EventConsumersDto.cs | 0 .../Controllers/History/HistoryController.cs | 0 .../History/Models/HistoryEventDto.cs | 0 .../Areas/Api/Controllers/LanguageDto.cs | 0 .../Languages/LanguagesController.cs | 0 .../Api/Controllers/News/Models/FeatureDto.cs | 0 .../Controllers/News/Models/FeaturesDto.cs | 0 .../Api/Controllers/News/MyNewsOptions.cs | 0 .../Api/Controllers/News/NewsController.cs | 0 .../News/Service/FeaturesService.cs | 0 .../Api/Controllers/Ping/PingController.cs | 0 .../Controllers/Plans/AppPlansController.cs | 93 + .../Controllers/Plans/Models/AppPlansDto.cs | 53 + .../Controllers/Plans/Models/ChangePlanDto.cs | 0 .../Plans/Models/PlanChangedDto.cs | 17 + .../Api/Controllers/Plans/Models/PlanDto.cs | 0 .../Converters/RuleTriggerDtoFactory.cs | 0 .../Controllers/Rules/Models/CreateRuleDto.cs | 0 .../Rules/Models/RuleActionConverter.cs | 0 .../Rules/Models/RuleActionProcessor.cs | 78 + .../Api/Controllers/Rules/Models/RuleDto.cs | 0 .../Rules/Models/RuleElementDto.cs | 0 .../Rules/Models/RuleElementPropertyDto.cs | 0 .../Controllers/Rules/Models/RuleEventDto.cs | 0 .../Controllers/Rules/Models/RuleEventsDto.cs | 0 .../Rules/Models/RuleTriggerDto.cs | 0 .../Api/Controllers/Rules/Models/RulesDto.cs | 0 .../Triggers/AssetChangedRuleTriggerDto.cs | 0 .../Triggers/ContentChangedRuleTriggerDto.cs | 0 .../ContentChangedRuleTriggerSchemaDto.cs | 0 .../Models/Triggers/ManualRuleTriggerDto.cs | 0 .../Triggers/SchemaChangedRuleTriggerDto.cs | 0 .../Models/Triggers/UsageRuleTriggerDto.cs | 0 .../Controllers/Rules/Models/UpdateRuleDto.cs | 0 .../Api/Controllers/Rules/RulesController.cs | 0 .../Controllers/Schemas/Models/AddFieldDto.cs | 0 .../Schemas/Models/ChangeCategoryDto.cs | 0 .../Schemas/Models/ConfigurePreviewUrlsDto.cs | 0 .../Converters/FieldPropertiesDtoFactory.cs | 0 .../Schemas/Models/CreateSchemaDto.cs | 0 .../Controllers/Schemas/Models/FieldDto.cs | 0 .../Schemas/Models/FieldPropertiesDto.cs | 75 + .../Models/Fields/ArrayFieldPropertiesDto.cs | 0 .../Models/Fields/AssetsFieldPropertiesDto.cs | 0 .../Fields/BooleanFieldPropertiesDto.cs | 0 .../Fields/DateTimeFieldPropertiesDto.cs | 0 .../Fields/GeolocationFieldPropertiesDto.cs | 0 .../Models/Fields/JsonFieldPropertiesDto.cs | 0 .../Models/Fields/NumberFieldPropertiesDto.cs | 0 .../Fields/ReferencesFieldPropertiesDto.cs | 0 .../Models/Fields/StringFieldPropertiesDto.cs | 0 .../Models/Fields/TagsFieldPropertiesDto.cs | 0 .../Models/Fields/UIFieldPropertiesDto.cs | 0 .../Schemas/Models/NestedFieldDto.cs | 0 .../Schemas/Models/ReorderFieldsDto.cs | 0 .../Schemas/Models/SchemaDetailsDto.cs | 0 .../Controllers/Schemas/Models/SchemaDto.cs | 0 .../Schemas/Models/SchemaPropertiesDto.cs | 0 .../Schemas/Models/SchemaScriptsDto.cs | 0 .../Controllers/Schemas/Models/SchemasDto.cs | 0 .../Schemas/Models/SynchronizeSchemaDto.cs | 0 .../Schemas/Models/UpdateFieldDto.cs | 26 + .../Schemas/Models/UpdateSchemaDto.cs | 0 .../Schemas/Models/UpsertSchemaDto.cs | 104 + .../Schemas/Models/UpsertSchemaFieldDto.cs | 0 .../Models/UpsertSchemaNestedFieldDto.cs | 0 .../Schemas/SchemaFieldsController.cs | 0 .../Controllers/Schemas/SchemasController.cs | 330 + .../Statistics/Models/CallsUsageDto.cs | 0 .../Statistics/Models/CurrentCallsDto.cs | 0 .../Statistics/Models/CurrentStorageDto.cs | 0 .../Statistics/Models/LogDownloadDto.cs | 0 .../Statistics/Models/StorageUsageDto.cs | 0 .../Statistics/UsagesController.cs | 0 .../Translations/Models/TranslateDto.cs | 0 .../Translations/Models/TranslationDto.cs | 0 .../Translations/TranslationsController.cs | 0 .../Controllers/UI/Models/UISettingsDto.cs | 0 .../Controllers/UI/Models/UpdateSettingDto.cs | 0 .../Areas/Api/Controllers/UI/MyUIOptions.cs | 0 .../Areas/Api/Controllers/UI/UIController.cs | 0 .../Api/Controllers/Users/Assets/Avatar.png | Bin .../Controllers/Users/Models/CreateUserDto.cs | 0 .../Controllers/Users/Models/ResourcesDto.cs | 0 .../Controllers/Users/Models/UpdateUserDto.cs | 0 .../Api/Controllers/Users/Models/UserDto.cs | 95 + .../Api/Controllers/Users/Models/UsersDto.cs | 0 .../Users/UserManagementController.cs | 129 + .../Api/Controllers/Users/UsersController.cs | 198 + backend/src/Squidex/Areas/Api/Startup.cs | 34 + .../Areas/Api/Views/Shared/Docs.cshtml | 0 .../Frontend/Middlewares/IndexExtensions.cs | 0 .../Frontend/Middlewares/IndexMiddleware.cs | 0 .../Frontend/Middlewares/WebpackMiddleware.cs | 75 + backend/src/Squidex/Areas/Frontend/Startup.cs | 88 + .../Config/Cert/IdentityCert.crt | 29 + .../Config/Cert/IdentityCert.key | 52 + .../Config/Cert/IdentityCert.pfx | Bin 0 -> 5389 bytes .../IdentityServer/Config/CreateAdminHost.cs | 77 + .../Config/IdentityServerExtensions.cs | 78 + .../Config/IdentityServerServices.cs | 120 + .../IdentityServer/Config/LazyClientStore.cs | 239 + .../Controllers/Account/AccountController.cs | 438 + .../Controllers/Account/ConsentModel.cs | 0 .../Controllers/Account/ConsentVM.cs | 16 + .../Controllers/Account/LoginModel.cs | 0 .../Controllers/Account/LoginVM.cs | 26 + .../Controllers/Error/ErrorController.cs | 63 + .../Controllers/Error/ErrorViewModel.cs | 0 .../IdentityServer/Controllers/Extensions.cs | 47 + .../Controllers/ExternalProvider.cs | 0 .../Controllers/IdentityServerController.cs | 0 .../Profile/ChangePasswordModel.cs | 0 .../Controllers/Profile/ChangeProfileModel.cs | 0 .../Controllers/Profile/ProfileController.cs | 235 + .../Controllers/Profile/ProfileVM.cs | 37 + .../Controllers/Profile/RemoveLoginModel.cs | 0 .../Controllers/Profile/SetPasswordModel.cs | 0 .../Squidex/Areas/IdentityServer/Startup.cs | 48 + .../Views/Account/AccessDenied.cshtml | 0 .../Views/Account/Consent.cshtml | 0 .../Views/Account/LockedOut.cshtml | 0 .../IdentityServer/Views/Account/Login.cshtml | 0 .../Views/Account/LogoutCompleted.cshtml | 0 .../IdentityServer/Views/Error/Error.cshtml | 0 .../Areas/IdentityServer/Views/Extensions.cs | 44 + .../Views/Profile/Profile.cshtml | 0 .../Areas/IdentityServer/Views/_Layout.cshtml | 0 .../IdentityServer/Views/_ViewImports.cshtml | 0 .../IdentityServer/Views/_ViewStart.cshtml | 0 ...rleansDashboardAuthenticationMiddleware.cs | 0 .../Squidex/Areas/OrleansDashboard/Startup.cs | 29 + ...PortalDashboardAuthenticationMiddleware.cs | 0 .../Middlewares/PortalRedirectMiddleware.cs | 39 + backend/src/Squidex/Areas/Portal/Startup.cs | 28 + .../Authentication/AuthenticationServices.cs | 44 + .../GithubAuthenticationServices.cs | 30 + .../Config/Authentication/GithubHandler.cs | 0 .../GoogleAuthenticationServices.cs | 30 + .../Config/Authentication/GoogleHandler.cs | 64 + .../Authentication/IdentityServerServices.cs | 65 + .../Config/Authentication/IdentityServices.cs | 29 + .../MicrosoftAuthenticationServices.cs | 30 + .../Config/Authentication/MicrosoftHandler.cs | 48 + .../Config/Authentication/OidcHandler.cs | 0 .../Config/Authentication/OidcServices.cs | 43 + .../src/Squidex/Config/Domain/AppsServices.cs | 57 + .../Squidex/Config/Domain/AssetServices.cs | 130 + .../Squidex/Config/Domain/BackupsServices.cs | 41 + .../Squidex/Config/Domain/CommandsServices.cs | 114 + .../Squidex/Config/Domain/CommentsServices.cs | 21 + .../Config/Domain/ConfigurationExtensions.cs | 20 + .../Squidex/Config/Domain/ContentsServices.cs | 58 + .../Config/Domain/EventPublishersServices.cs | 68 + .../Config/Domain/EventSourcingServices.cs | 102 + .../Config/Domain/HealthCheckServices.cs | 28 + .../Squidex/Config/Domain/HistoryServices.cs | 22 + .../Config/Domain/InfrastructureServices.cs | 116 + .../Squidex/Config/Domain/LoggingServices.cs | 129 + .../Config/Domain/MigrationServices.cs | 72 + .../Config/Domain/NotificationsServices.cs | 46 + .../Squidex/Config/Domain/QueryServices.cs | 51 + .../src/Squidex/Config/Domain/RuleServices.cs | 70 + .../Squidex/Config/Domain/SchemasServices.cs | 22 + .../Config/Domain/SerializationInitializer.cs | 0 .../Config/Domain/SerializationServices.cs | 124 + .../Squidex/Config/Domain/StoreServices.cs | 126 + .../Config/Domain/SubscriptionServices.cs | 35 + .../src}/Squidex/Config/MyIdentityOptions.cs | 0 .../src}/Squidex/Config/Orleans/Helper.cs | 0 .../Squidex/Config/Orleans/OrleansServices.cs | 125 + .../Squidex/Config/Startup/BackgroundHost.cs | 38 + .../Squidex/Config/Startup/InitializerHost.cs | 37 + .../Config/Startup/LogConfigurationHost.cs | 51 + .../Config/Startup/MigrationRebuilderHost.cs | 30 + .../Squidex/Config/Startup/MigratorHost.cs | 30 + .../Config/Startup/SafeHostedService.cs | 48 + .../src/Squidex/Config/Web/WebExtensions.cs | 121 + backend/src/Squidex/Config/Web/WebServices.cs | 76 + .../src}/Squidex/Docs/schemabody.md | 0 .../src}/Squidex/Docs/schemaquery.md | 0 {src => backend/src}/Squidex/Docs/security.md | 0 .../Squidex/Pipeline/OpenApi/NSwagHelper.cs | 114 + .../src}/Squidex/Pipeline/Plugins/MvcParts.cs | 0 .../Pipeline/Plugins/PluginExtensions.cs | 81 + .../Squidex/Pipeline/Plugins/PluginLoaders.cs | 84 + .../Pipeline/Robots/RobotsTxtMiddleware.cs | 46 + .../Pipeline/Robots/RobotsTxtOptions.cs | 0 .../Squidex/Pipeline/Squid/SquidMiddleware.cs | 144 + .../Squidex/Pipeline/Squid/icon-happy-sm.svg | 0 .../Squidex/Pipeline/Squid/icon-happy.svg | 0 .../Squidex/Pipeline/Squid/icon-sad-sm.svg | 0 .../src}/Squidex/Pipeline/Squid/icon-sad.svg | 0 backend/src/Squidex/Program.cs | 67 + backend/src/Squidex/Squidex.csproj | 130 + backend/src/Squidex/Startup.cs | 93 + {src => backend/src}/Squidex/appsettings.json | 0 .../wwwroot/client-callback-popup.html | 0 .../wwwroot/client-callback-silent.html | 0 .../src}/Squidex/wwwroot/favicon.ico | Bin .../src}/Squidex/wwwroot/images/add-app.png | Bin .../src}/Squidex/wwwroot/images/add-blog.png | Bin .../Squidex/wwwroot/images/add-identity.png | Bin .../Squidex/wwwroot/images/add-profile.png | Bin .../src}/Squidex/wwwroot/images/asset_doc.png | Bin .../Squidex/wwwroot/images/asset_docx.png | Bin .../Squidex/wwwroot/images/asset_generic.png | Bin .../src}/Squidex/wwwroot/images/asset_pdf.png | Bin .../src}/Squidex/wwwroot/images/asset_ppt.png | Bin .../Squidex/wwwroot/images/asset_pptx.png | Bin .../Squidex/wwwroot/images/asset_video.png | Bin .../src}/Squidex/wwwroot/images/asset_xls.png | Bin .../Squidex/wwwroot/images/asset_xlsx.png | Bin .../src}/Squidex/wwwroot/images/client.png | Bin .../src}/Squidex/wwwroot/images/client.svg | 0 .../Squidex/wwwroot/images/dashboard-api.png | Bin .../wwwroot/images/dashboard-feedback.png | Bin .../wwwroot/images/dashboard-github.png | Bin .../wwwroot/images/dashboard-schema.png | Bin .../Squidex/wwwroot/images/loader-white.gif | Bin .../src}/Squidex/wwwroot/images/loader.gif | Bin .../Squidex/wwwroot/images/login-icon.png | Bin .../src}/Squidex/wwwroot/images/logo-half.png | Bin .../Squidex/wwwroot/images/logo-small.png | Bin .../wwwroot/images/logo-squared-120.png | Bin .../wwwroot/images/logo-white-small.png | Bin .../Squidex/wwwroot/images/logo-white.png | Bin .../src}/Squidex/wwwroot/images/logo.png | Bin .../wwwroot/images/onboarding-background.png | Bin .../wwwroot/images/onboarding-step1.png | Bin .../wwwroot/images/onboarding-step2.png | Bin .../wwwroot/images/onboarding-step3.png | Bin .../wwwroot/images/onboarding-step4.png | Bin .../wwwroot/scripts/combined-editor.html | 0 .../wwwroot/scripts/context-editor.html | 0 .../Squidex/wwwroot/scripts/editor-sdk.js | 0 .../wwwroot/scripts/oidc-client.min.js | 47 + .../wwwroot/scripts/simple-editor.html | 0 stylecop.json => backend/stylecop.json | 0 {tests => backend/tests}/RunCoverage.ps1 | 0 .../Model/Apps/AppClientJsonTests.cs | 0 .../Model/Apps/AppClientsTests.cs | 0 .../Model/Apps/AppContributorsJsonTests.cs | 0 .../Model/Apps/AppContributorsTests.cs | 0 .../Model/Apps/AppImageTests.cs | 0 .../Model/Apps/AppPatternJsonTests.cs | 0 .../Model/Apps/AppPatternsTests.cs | 0 .../Model/Apps/AppPlanTests.cs | 0 .../Model/Apps/LanguagesConfigJsonTests.cs | 0 .../Model/Apps/LanguagesConfigTests.cs | 0 .../Model/Apps/RoleTests.cs | 79 + .../Model/Apps/RolesJsonTests.cs | 0 .../Model/Apps/RolesTests.cs | 162 + .../Model/Contents/ContentDataTests.cs | 0 .../Model/Contents/ContentFieldDataTests.cs | 0 .../Model/Contents/StatusTests.cs | 0 .../Model/Contents/WorkflowJsonTests.cs | 39 + .../Model/Contents/WorkflowTests.cs | 147 + .../Model/Contents/WorkflowsJsonTests.cs | 0 .../Model/Contents/WorkflowsTests.cs | 0 .../Model/InvariantPartitionTests.cs | 0 .../Model/PartitioningTests.cs | 85 + .../Model/Rules/RuleTests.cs | 169 + .../Model/Schemas/ArrayFieldTests.cs | 0 .../Model/Schemas/SchemaFieldTests.cs | 116 + .../Model/Schemas/SchemaTests.cs | 0 .../ContentConversionFlatTests.cs | 148 + .../ConvertContent/ContentConversionTests.cs | 0 .../ConvertContent/FieldConvertersTests.cs | 0 .../ConvertContent/ValueConvertersTests.cs | 0 .../EnrichContent/ContentEnrichmentTests.cs | 198 + .../EventSynchronization/AssertHelper.cs | 0 .../SchemaSynchronizerTests.cs | 606 + .../ReferenceExtractionTests.cs | 308 + .../ReferenceFormattingTests.cs | 0 .../Operations/GenerateEdmSchema/EdmTests.cs | 0 .../GenerateJsonSchema/JsonSchemaTests.cs | 0 .../HandleRules/RuleElementRegistryTests.cs | 0 .../HandleRules/RuleEventFormatterTests.cs | 0 .../HandleRules/RuleServiceTests.cs | 331 + .../Scripting/ContentDataObjectTests.cs | 0 .../Scripting/JintScriptEngineTests.cs | 0 .../Operations/Scripting/JintUserTests.cs | 0 .../Operations/Tags/TagNormalizerTests.cs | 134 + .../ValidateContent/ArrayFieldTests.cs | 125 + .../ValidateContent/AssetsFieldTests.cs | 321 + .../ValidateContent/BooleanFieldTests.cs | 0 .../ValidateContent/ContentValidationTests.cs | 0 .../ValidateContent/DateTimeFieldTests.cs | 0 .../ValidateContent/GeolocationFieldTests.cs | 0 .../ValidateContent/JsonFieldTests.cs | 0 .../ValidateContent/NumberFieldTests.cs | 0 .../ValidateContent/ReferencesFieldTests.cs | 192 + .../ValidateContent/StringFieldTests.cs | 139 + .../ValidateContent/TagsFieldTests.cs | 159 + .../ValidateContent/UIFieldTests.cs | 129 + .../ValidationTestExtensions.cs | 86 + .../Validators/AllowedValuesValidatorTests.cs | 0 .../CollectionItemValidatorTests.cs | 0 .../Validators/CollectionValidatorTests.cs | 0 .../Validators/NoValueValidatorTests.cs | 0 .../Validators/PatternValidatorTests.cs | 0 .../Validators/RangeValidatorTests.cs | 0 .../RequiredStringValidatorTests.cs | 0 .../Validators/RequiredValidatorTests.cs | 0 .../Validators/StringLengthValidatorTests.cs | 0 .../Validators/UniqueValidatorTests.cs | 0 .../Validators/UniqueValuesValidatorTests.cs | 0 .../Squidex.Domain.Apps.Core.Tests.csproj | 33 + .../TestUtils.cs | 173 + .../Apps/AppCommandMiddlewareTests.cs | 102 + .../Apps/AppGrainTests.cs | 659 + .../Apps/AppUISettingsGrainTests.cs | 0 .../Apps/AppUISettingsTests.cs | 0 .../Billing/ConfigAppLimitsProviderTests.cs | 0 .../Billing/NoopAppPlanBillingManagerTests.cs | 36 + .../Apps/Guards/GuardAppClientsTests.cs | 0 .../Apps/Guards/GuardAppContributorsTests.cs | 223 + .../Apps/Guards/GuardAppLanguagesTests.cs | 0 .../Apps/Guards/GuardAppPatternsTests.cs | 0 .../Apps/Guards/GuardAppRolesTests.cs | 165 + .../Apps/Guards/GuardAppTests.cs | 132 + .../Apps/Guards/GuardAppWorkflowTests.cs | 213 + .../Apps/Indexes/AppsIndexTests.cs | 387 + .../InviteUserCommandMiddlewareTests.cs | 0 .../Apps/RolePermissionsProviderTests.cs | 0 ...lwaysCreateClientCommandMiddlewareTests.cs | 0 .../Apps/Templates/TemplatesTests.cs | 0 .../Assets/AssetChangedTriggerHandlerTests.cs | 149 + .../Assets/AssetCommandMiddlewareTests.cs | 0 .../Assets/AssetGrainTests.cs | 0 .../Assets/FileTypeTagGeneratorTests.cs | 57 + .../Assets/Guards/GuardAssetTests.cs | 0 .../Assets/ImageTagGeneratorTests.cs | 0 .../Assets/MongoDb/MongoDbQueryTests.cs | 236 + .../Assets/Queries/AssetEnricherTests.cs | 0 .../Assets/Queries/AssetLoaderTests.cs | 66 + .../Assets/Queries/AssetQueryParserTests.cs | 0 .../Assets/Queries/AssetQueryServiceTests.cs | 0 .../Queries/FilterTagTransformerTests.cs | 61 + .../Backup/BackupReaderWriterTests.cs | 0 .../Comments/CommentsGrainTests.cs | 0 .../Comments/CommentsLoaderTests.cs | 0 .../Comments/Guards/GuardCommentsTests.cs | 0 .../ContentChangedTriggerHandlerTests.cs | 235 + .../Contents/ContentCommandMiddlewareTests.cs | 0 .../Contents/ContentGrainTests.cs | 592 + .../Contents/DefaultContentWorkflowTests.cs | 139 + .../DefaultWorkflowsValidatorTests.cs | 113 + .../Contents/DynamicContentWorkflowTests.cs | 352 + .../Contents/GraphQL/GraphQLQueriesTests.cs | 1270 ++ .../Contents/GraphQL/GraphQLTestBase.cs | 287 + .../Contents/Guard/GuardContentTests.cs | 0 .../Contents/MongoDb/MongoDbQueryTests.cs | 289 + .../Contents/MongoDb/StatusSerializerTests.cs | 0 .../Queries/ContentEnricherAssetsTests.cs | 0 .../Queries/ContentEnricherReferencesTests.cs | 0 .../Contents/Queries/ContentEnricherTests.cs | 204 + .../Contents/Queries/ContentLoaderTests.cs | 77 + .../Queries/ContentQueryParserTests.cs | 0 .../Queries/ContentQueryServiceTests.cs | 503 + .../Queries/FilterTagTransformerTests.cs | 105 + .../SingletonCommandMiddlewareTests.cs | 0 .../Contents/TestData/FakeUrlGenerator.cs | 0 .../Contents/Text/GrainTextIndexerTests.cs | 0 .../Contents/Text/TextIndexerGrainTests.cs | 263 + .../NotificationEmailEventConsumerTests.cs | 191 + .../NotificationEmailSenderTests.cs | 0 .../Rules/Guards/GuardRuleTests.cs | 188 + .../Triggers/ContentChangedTriggerTests.cs | 108 + .../Triggers/UsageTriggerValidationTests.cs | 0 .../Rules/Indexes/RulesIndexTests.cs | 0 .../Rules/ManualTriggerHandlerTests.cs | 39 + .../Rules/Queries/RuleEnricherTests.cs | 0 .../Rules/Queries/RuleQueryServiceTests.cs | 0 .../Rules/RuleCommandMiddlewareTests.cs | 0 .../Rules/RuleDequeuerTests.cs | 0 .../Rules/RuleEnqueuerTests.cs | 118 + .../Rules/RuleGrainTests.cs | 0 .../UsageTracking/UsageTriggerHandlerTests.cs | 68 + .../ArrayFieldPropertiesTests.cs | 0 .../AssetsFieldPropertiesTests.cs | 0 .../BooleanFieldPropertiesTests.cs | 0 .../DateTimeFieldPropertiesTests.cs | 0 .../GeolocationFieldPropertiesTests.cs | 0 .../JsonFieldPropertiesTests.cs | 0 .../NumberFieldPropertiesTests.cs | 0 .../ReferencesFieldPropertiesTests.cs | 0 .../StringFieldPropertiesTests.cs | 0 .../TagsFieldPropertiesTests.cs | 0 .../FieldProperties/UIFieldPropertiesTests.cs | 0 .../Schemas/Guards/GuardSchemaFieldTests.cs | 379 + .../Schemas/Guards/GuardSchemaTests.cs | 530 + .../Schemas/Indexes/SchemasIndexTests.cs | 248 + .../SchemaChangedTriggerHandlerTests.cs | 146 + .../Schemas/SchemaGrainTests.cs | 0 .../Squidex.Domain.Apps.Entities.Tests.csproj | 40 + .../Tags/GrainTagServiceTests.cs | 0 .../Tags/TagGrainTests.cs | 0 .../TestHelpers/AExtensions.cs | 30 + .../TestHelpers/AssertHelper.cs | 0 .../TestHelpers/HandlerTestBase.cs | 0 .../TestHelpers/JsonHelper.cs | 68 + .../TestHelpers/Mocks.cs | 77 + .../TestHelpers/ValidationAssert.cs | 0 .../AssetUserPictureStoreTests.cs | 0 .../DefaultUserResolverTests.cs | 135 + .../DefaultXmlRepositoryTests.cs | 0 .../Squidex.Domain.Users.Tests.csproj | 34 + .../Assets/AssetExtensionTests.cs | 0 .../Assets/AssetStoreTests.cs | 164 + .../Assets/AzureBlobAssetStoreFixture.cs | 0 .../Assets/AzureBlobAssetStoreTests.cs | 0 .../Assets/FTPAssetStoreFixture.cs | 0 .../Assets/FTPAssetStoreTests.cs | 0 .../Assets/FolderAssetStoreFixture.cs | 0 .../Assets/FolderAssetStoreTests.cs | 0 .../Assets/GoogleCloudAssetStoreFixture.cs | 0 .../Assets/GoogleCloudAssetStoreTests.cs | 0 .../Assets/HasherStreamTests.cs | 0 .../ImageSharpAssetThumbnailGeneratorTests.cs | 92 + .../Assets/Images/logo.jpg | Bin .../Assets/Images/logo.png | Bin .../Assets/MemoryAssetStoreTests.cs | 0 .../Assets/MongoGridFSAssetStoreFixture.cs | 0 .../Assets/MongoGridFsAssetStoreTests.cs | 0 .../Caching/AsyncLocalCacheTests.cs | 0 .../Caching/LRUCacheTests.cs | 0 .../CollectionExtensionsTests.cs | 278 + .../Commands/CommandContextTests.cs | 0 .../CustomCommandMiddlewareRunnerTests.cs | 0 .../DomainObjectGrainFormatterTests.cs | 66 + .../Commands/DomainObjectGrainTests.cs | 220 + ...richWithTimestampCommandMiddlewareTests.cs | 0 .../Commands/InMemoryCommandBusTests.cs | 0 .../Commands/LogCommandMiddlewareTests.cs | 0 .../LogSnapshotDomainObjectGrainTests.cs | 280 + .../ReadonlyCommandMiddlewareTests.cs | 0 .../DisposableObjectBaseTests.cs | 0 .../DomainObjectExceptionTests.cs | 0 .../CompoundEventConsumerTests.cs | 0 .../CosmosDbEventStoreFixture.cs | 0 .../EventSourcing/CosmosDbEventStoreTests.cs | 0 .../DefaultEventDataFormatterTests.cs | 0 .../EventSourcing/EnvelopeExtensionsTests.cs | 0 .../EventSourcing/EnvelopeHeadersTests.cs | 0 .../EventSourcing/EnvelopeTests.cs | 0 .../EventSourcing/EventStoreTests.cs | 379 + .../EventSourcing/GetEventStoreFixture.cs | 0 .../EventSourcing/GetEventStoreTests.cs | 0 .../Grains/EventConsumerGrainTests.cs | 409 + .../Grains/EventConsumerManagerGrainTests.cs | 186 + .../Grains/OrleansEventNotifierTests.cs | 0 .../EventSourcing/MongoEventStoreFixture.cs | 0 .../EventSourcing/MongoEventStoreTests.cs | 0 .../EventSourcing/PollingSubscriptionTests.cs | 0 .../EventSourcing/RetrySubscriptionTests.cs | 124 + .../WrongEventVersionExceptionTests.cs | 0 .../FileExtensionsTests.cs | 0 .../GravatarHelperTests.cs | 0 .../GuardTests.cs | 367 + .../Http/DumpFormatterTests.cs | 131 + .../InstantExtensions.cs | 0 .../Json/ClaimsPrincipalConverterTests.cs | 55 + .../Json/InstantConverterTests.cs | 0 .../ConverterContractResolverTests.cs | 0 .../Newtonsoft/ReadOnlyCollectionTests.cs | 0 .../Json/Objects/JsonObjectTests.cs | 357 + .../Objects/JsonValuesSerializationTests.cs | 0 .../LanguageTests.cs | 141 + .../LanguagesInitializerTests.cs | 61 + .../Log/JsonLogWriterTests.cs | 0 .../Log/LockingLogStoreTests.cs | 87 + .../Log/SemanticLogAdapterTests.cs | 0 .../Log/SemanticLogTests.cs | 525 + .../Migrations/MigratorTests.cs | 167 + .../MongoDb/BsonConverterTests.cs | 0 .../MongoDb/MongoExtensionsTests.cs | 169 + .../NamedIdTests.cs | 140 + .../Net/IPAddressComparerTests.cs | 0 .../Orleans/ActivationLimiterFilterTests.cs | 0 .../Orleans/ActivationLimiterTests.cs | 0 .../Orleans/BootstrapTests.cs | 0 .../Orleans/Indexes/IdsIndexGrainTests.cs | 0 .../Indexes/UniqueNameIndexGrainTests.cs | 197 + .../Orleans/JsonExternalSerializerTests.cs | 119 + .../Orleans/LockGrainTests.cs | 50 + .../Orleans/LoggingFilterTests.cs | 0 .../Queries/JsonQueryConversionTests.cs | 382 + .../Queries/PascalCasePathConverterTests.cs | 32 + .../Queries/QueryJsonConversionTests.cs | 374 + .../Queries/QueryJsonTests.cs | 0 .../Queries/QueryODataConversionTests.cs | 424 + .../Queries/QueryOptimizationTests.cs | 94 + .../RandomHashTests.cs | 0 .../RefTokenTests.cs | 122 + .../Reflection/PropertiesTypeAccessorTests.cs | 0 .../Reflection/ReflectionExtensionTests.cs | 0 .../Reflection/SimpleCopierTests.cs | 0 .../Reflection/SimpleMapperTests.cs | 177 + .../RetryWindowTests.cs | 0 .../Security/ExtensionsTests.cs | 68 + .../Security/PermissionSetTests.cs | 0 .../Security/PermissionTests.cs | 0 .../Squidex.Infrastructure.Tests.csproj | 46 + .../States/DefaultStreamNameResolverTests.cs | 0 .../States/InconsistentStateExceptionTests.cs | 32 + .../States/PersistenceEventSourcingTests.cs | 0 .../States/PersistenceSnapshotTests.cs | 0 .../StringExtensionsTests.cs | 0 .../TaskExtensionsTests.cs | 0 .../Tasks/AsyncLockPoolTests.cs | 0 .../Tasks/AsyncLockTests.cs | 0 .../Tasks/PartitionedActionBlockTests.cs | 0 .../Tasks/SingleThreadedDispatcherTests.cs | 0 .../TestHelpers/BinaryFormatterHelper.cs | 0 .../TestHelpers/JsonHelper.cs | 68 + .../TestHelpers/MyCommand.cs | 0 .../TestHelpers/MyDomainObject.cs | 80 + .../TestHelpers/MyDomainState.cs | 0 .../TestHelpers/MyEvent.cs | 0 .../TestHelpers/MyGrain.cs | 29 + .../Timers/CompletionTimerTests.cs | 0 .../TypeNameAttributeTests.cs | 0 .../TypeNameRegistryTests.cs | 0 .../BackgroundUsageTrackerTests.cs | 228 + .../UsageTracking/CachingUsageTrackerTests.cs | 0 .../ValidationExceptionTests.cs | 81 + .../ValidationExtensionsTests.cs | 0 .../ApiCostsAttributeTests.cs | 0 .../ApiExceptionFilterAttributeTests.cs | 123 + .../ApiPermissionAttributeTests.cs | 113 + .../ETagCommandMiddlewareTests.cs | 119 + .../EnrichWithActorCommandMiddlewareTests.cs | 114 + .../EnrichWithAppIdCommandMiddlewareTests.cs | 104 + ...nrichWithSchemaIdCommandMiddlewareTests.cs | 157 + .../Squidex.Web.Tests/ExposedValuesTests.cs | 0 .../Pipeline/ApiCostsFilterTests.cs | 167 + .../Pipeline/ApiPermissionUnifierTests.cs | 0 .../Pipeline/AppResolverTests.cs | 198 + .../Pipeline/CleanupHostMiddlewareTests.cs | 0 .../Pipeline/ETagFilterTests.cs | 102 + .../Pipeline/EnforceHttpsMiddlewareTests.cs | 0 .../Squidex.Web.Tests.csproj | 33 + {tests => backend/tests}/docker-compose.yml | 0 .../GenerateLanguages.csproj | 16 + .../GenerateLanguages/GenerateLanguages.sln | 0 .../tools}/GenerateLanguages/Program.cs | 0 backend/tools/LoadTest/LoadTest.csproj | 23 + .../tools}/LoadTest/LoadTest.sln | 0 .../tools}/LoadTest/Model/TestClient.cs | 0 .../tools}/LoadTest/Model/TestEntity.cs | 0 .../tools}/LoadTest/ReadingBenchmarks.cs | 0 .../tools}/LoadTest/ReadingFixture.cs | 0 {tools => backend/tools}/LoadTest/Run.cs | 0 .../tools}/LoadTest/TestUtils.cs | 0 .../tools}/LoadTest/Utils/Run.cs | 0 .../tools}/LoadTest/WritingBenchmarks.cs | 0 .../tools}/LoadTest/WritingFixture.cs | 0 backend/tools/Migrate_00/Migrate_00.csproj | 19 + .../tools}/Migrate_00/Program.cs | 0 backend/tools/Migrate_01/Migrate_01.csproj | 25 + backend/tools/Migrate_01/MigrationPath.cs | 132 + .../Migrate_01/Migrations/AddPatterns.cs | 60 + .../Migrate_01/Migrations/ClearSchemas.cs | 0 .../Migrations/ConvertEventStore.cs | 69 + .../Migrations/ConvertEventStoreAppId.cs | 97 + .../Migrate_01/Migrations/CreateAssetSlugs.cs | 0 .../MongoDb/ConvertOldSnapshotStores.cs | 0 .../MongoDb/ConvertRuleEventsJson.cs | 0 .../MongoDb/DeleteContentCollections.cs | 0 .../MongoDb/RenameAssetSlugField.cs | 0 .../MongoDb/RestructureContentCollection.cs | 0 .../Migrations/PopulateGrainIndexes.cs | 0 .../Migrate_01/Migrations/RebuildApps.cs | 0 .../Migrate_01/Migrations/RebuildAssets.cs | 0 .../Migrate_01/Migrations/RebuildContents.cs | 0 .../Migrate_01/Migrations/RebuildSnapshots.cs | 0 .../Migrations/StartEventConsumers.cs | 0 .../Migrations/StopEventConsumers.cs | 0 .../Migrate_01/OldEvents/AppClientChanged.cs | 0 .../OldEvents/AppClientPermission.cs | 0 .../Migrate_01/OldEvents/AppClientUpdated.cs | 0 .../OldEvents/AppContributorAssigned.cs | 0 .../OldEvents/AppContributorPermission.cs | 0 .../Migrate_01/OldEvents/AppPlanChanged.cs | 0 .../OldEvents/AppWorkflowConfigured.cs | 0 .../Migrate_01/OldEvents/AssetRenamed.cs | 0 .../Migrate_01/OldEvents/AssetTagged.cs | 0 .../Migrate_01/OldEvents/ContentArchived.cs | 0 .../Migrate_01/OldEvents/ContentCreated.cs | 0 .../Migrate_01/OldEvents/ContentPublished.cs | 0 .../Migrate_01/OldEvents/ContentRestored.cs | 0 .../OldEvents/ContentStatusChanged.cs | 0 .../OldEvents/ContentUnpublished.cs | 0 .../Migrate_01/OldEvents/SchemaCreated.cs | 0 .../Migrate_01/OldEvents/ScriptsConfigured.cs | 0 .../Migrate_01/OldEvents/WebhookAdded.cs | 0 .../Migrate_01/OldEvents/WebhookDeleted.cs | 0 .../OldTriggers/AssetChangedTrigger.cs | 0 .../OldTriggers/ContentChangedTrigger.cs | 0 .../ContentChangedTriggerSchema.cs | 0 .../tools}/Migrate_01/RebuildOptions.cs | 0 backend/tools/Migrate_01/RebuildRunner.cs | 66 + .../tools}/Migrate_01/Rebuilder.cs | 0 .../tools}/Migrate_01/SquidexMigrations.cs | 0 build.ps1 | 4 +- build.sh | 4 +- .../Actions/Algolia/AlgoliaActionHandler.cs | 134 - .../Actions/Fastly/FastlyActionHandler.cs | 70 - .../Actions/Slack/SlackActionHandler.cs | 68 - .../Actions/Twitter/TweetActionHandler.cs | 72 - .../Actions/Webhook/WebhookActionHandler.cs | 86 - .../Squidex.Extensions.csproj | 32 - {src/Squidex => frontend}/.sass-lint.yml | 0 .../app-config/karma-test-shim.js | 0 .../app-config/karma.conf.js | 0 .../app-config/karma.coverage.conf.js | 0 frontend/app-config/webpack.config.js | 376 + .../wwwroot => frontend/app}/_theme.html | 0 .../app/app.component.html | 0 .../app/app.component.scss | 0 .../Squidex => frontend}/app/app.component.ts | 0 {src/Squidex => frontend}/app/app.module.ts | 0 {src/Squidex => frontend}/app/app.routes.ts | 0 {src/Squidex => frontend}/app/app.ts | 0 .../app/declarations.d.ts | 0 .../administration-area.component.html | 0 .../administration-area.component.scss | 0 .../administration-area.component.ts | 0 .../features/administration/declarations.ts | 0 .../guards/unset-user.guard.spec.ts | 0 .../administration/guards/unset-user.guard.ts | 0 .../guards/user-must-exist.guard.spec.ts | 0 .../guards/user-must-exist.guard.ts | 0 .../app/features/administration/internal.ts | 0 .../app/features/administration/module.ts | 0 .../event-consumer.component.ts | 0 .../event-consumers-page.component.html | 0 .../event-consumers-page.component.scss | 0 .../event-consumers-page.component.ts | 0 .../pages/restore/restore-page.component.html | 0 .../pages/restore/restore-page.component.scss | 0 .../pages/restore/restore-page.component.ts | 0 .../pages/users/user-page.component.html | 0 .../pages/users/user-page.component.scss | 0 .../pages/users/user-page.component.ts | 0 .../pages/users/user.component.ts | 0 .../pages/users/users-page.component.html | 0 .../pages/users/users-page.component.scss | 0 .../pages/users/users-page.component.ts | 0 .../services/event-consumers.service.spec.ts | 0 .../services/event-consumers.service.ts | 0 .../services/users.service.spec.ts | 0 .../administration/services/users.service.ts | 0 .../state/event-consumers.state.spec.ts | 0 .../state/event-consumers.state.ts | 0 .../administration/state/users.forms.ts | 0 .../administration/state/users.state.spec.ts | 0 .../administration/state/users.state.ts | 0 .../app/features/api/api-area.component.html | 0 .../app/features/api/api-area.component.scss | 0 .../app/features/api/api-area.component.ts | 0 .../app/features/api/declarations.ts | 0 .../app/features/api/index.ts | 0 .../app/features/api/module.ts | 0 .../pages/graphql/graphql-page.component.html | 0 .../pages/graphql/graphql-page.component.scss | 0 .../pages/graphql/graphql-page.component.ts | 0 .../app/features/apps/declarations.ts | 0 .../app/features/apps/index.ts | 0 .../app/features/apps/module.ts | 0 .../apps/pages/apps-page.component.html | 0 .../apps/pages/apps-page.component.scss | 0 .../apps/pages/apps-page.component.ts | 0 .../apps/pages/news-dialog.component.html | 0 .../apps/pages/news-dialog.component.scss | 0 .../apps/pages/news-dialog.component.ts | 0 .../pages/onboarding-dialog.component.html | 0 .../pages/onboarding-dialog.component.scss | 0 .../apps/pages/onboarding-dialog.component.ts | 0 .../app/features/assets/declarations.ts | 0 .../app/features/assets/index.ts | 0 .../app/features/assets/module.ts | 0 .../pages/assets-filters-page.component.html | 0 .../pages/assets-filters-page.component.scss | 0 .../pages/assets-filters-page.component.ts | 0 .../assets/pages/assets-page.component.html | 0 .../assets/pages/assets-page.component.scss | 0 .../assets/pages/assets-page.component.ts | 0 .../app/features/content/declarations.ts | 0 .../app/features/content/index.ts | 0 .../app/features/content/module.ts | 0 .../comments/comments-page.component.html | 0 .../comments/comments-page.component.scss | 0 .../pages/comments/comments-page.component.ts | 0 .../content/content-field.component.html | 0 .../content/content-field.component.scss | 0 .../pages/content/content-field.component.ts | 0 .../content-history-page.component.html | 0 .../content-history-page.component.scss | 0 .../content/content-history-page.component.ts | 0 .../pages/content/content-page.component.html | 0 .../pages/content/content-page.component.scss | 0 .../pages/content/content-page.component.ts | 0 .../content/field-languages.component.ts | 0 .../contents-filters-page.component.html | 0 .../contents-filters-page.component.scss | 0 .../contents-filters-page.component.ts | 0 .../contents/contents-page.component.html | 0 .../contents/contents-page.component.scss | 0 .../pages/contents/contents-page.component.ts | 0 .../app/features/content/pages/messages.ts | 0 .../pages/schemas/schemas-page.component.html | 0 .../pages/schemas/schemas-page.component.scss | 0 .../pages/schemas/schemas-page.component.ts | 0 .../shared/array-editor.component.html | 0 .../shared/array-editor.component.scss | 0 .../content/shared/array-editor.component.ts | 0 .../content/shared/array-item.component.html | 0 .../content/shared/array-item.component.scss | 0 .../content/shared/array-item.component.ts | 0 .../shared/assets-editor.component.html | 0 .../shared/assets-editor.component.scss | 0 .../content/shared/assets-editor.component.ts | 0 .../shared/content-selector-item.component.ts | 0 .../shared/content-status.component.html | 0 .../shared/content-status.component.scss | 0 .../shared/content-status.component.ts | 0 .../shared/content-value-editor.component.ts | 0 .../content/shared/content-value.component.ts | 0 .../content/shared/content.component.html | 0 .../content/shared/content.component.scss | 0 .../content/shared/content.component.ts | 0 .../shared/contents-selector.component.html | 0 .../shared/contents-selector.component.scss | 0 .../shared/contents-selector.component.ts | 0 .../shared/due-time-selector.component.html | 0 .../shared/due-time-selector.component.scss | 0 .../shared/due-time-selector.component.ts | 0 .../shared/field-editor.component.html | 0 .../shared/field-editor.component.scss | 0 .../content/shared/field-editor.component.ts | 0 .../shared/preview-button.component.html | 0 .../shared/preview-button.component.scss | 0 .../shared/preview-button.component.ts | 0 .../shared/reference-item.component.scss | 0 .../shared/reference-item.component.ts | 0 .../shared/references-editor.component.html | 0 .../shared/references-editor.component.scss | 0 .../shared/references-editor.component.ts | 0 .../app/features/dashboard/declarations.ts | 0 .../app/features/dashboard/index.ts | 0 .../app/features/dashboard/module.ts | 0 .../pages/dashboard-page.component.html | 0 .../pages/dashboard-page.component.scss | 0 .../pages/dashboard-page.component.ts | 0 .../app/features/rules/declarations.ts | 0 .../app/features/rules/index.ts | 0 .../app/features/rules/module.ts | 0 .../app/features/rules/pages/events/pipes.ts | 0 .../events/rule-events-page.component.html | 0 .../events/rule-events-page.component.scss | 0 .../events/rule-events-page.component.ts | 0 .../actions/generic-action.component.html | 0 .../actions/generic-action.component.scss | 0 .../rules/actions/generic-action.component.ts | 0 .../pages/rules/rule-element.component.html | 0 .../pages/rules/rule-element.component.scss | 0 .../pages/rules/rule-element.component.ts | 0 .../rules/pages/rules/rule-icon.component.ts | 0 .../pages/rules/rule-wizard.component.html | 0 .../pages/rules/rule-wizard.component.scss | 0 .../pages/rules/rule-wizard.component.ts | 0 .../rules/pages/rules/rule.component.html | 0 .../rules/pages/rules/rule.component.scss | 0 .../rules/pages/rules/rule.component.ts | 0 .../pages/rules/rules-page.component.html | 0 .../pages/rules/rules-page.component.scss | 0 .../rules/pages/rules/rules-page.component.ts | 0 .../asset-changed-trigger.component.html | 0 .../asset-changed-trigger.component.scss | 0 .../asset-changed-trigger.component.ts | 0 .../content-changed-trigger.component.html | 0 .../content-changed-trigger.component.scss | 0 .../content-changed-trigger.component.ts | 0 .../schema-changed-trigger.component.html | 0 .../schema-changed-trigger.component.scss | 0 .../schema-changed-trigger.component.ts | 0 .../triggers/usage-trigger.component.html | 0 .../triggers/usage-trigger.component.scss | 0 .../rules/triggers/usage-trigger.component.ts | 0 .../app/features/schemas/declarations.ts | 0 .../app/features/schemas/index.ts | 0 .../app/features/schemas/module.ts | 0 .../app/features/schemas/pages/messages.ts | 0 .../pages/schema/field-wizard.component.html | 0 .../pages/schema/field-wizard.component.scss | 0 .../pages/schema/field-wizard.component.ts | 0 .../schemas/pages/schema/field.component.html | 0 .../schemas/pages/schema/field.component.scss | 0 .../schemas/pages/schema/field.component.ts | 0 .../forms/field-form-common.component.ts | 0 .../schema/forms/field-form-ui.component.ts | 0 .../forms/field-form-validation.component.ts | 0 .../schema/forms/field-form.component.ts | 0 .../schema/schema-edit-form.component.html | 0 .../schema/schema-edit-form.component.scss | 0 .../schema/schema-edit-form.component.ts | 0 .../schema/schema-export-form.component.html | 0 .../schema/schema-export-form.component.scss | 0 .../schema/schema-export-form.component.ts | 0 .../pages/schema/schema-page.component.html | 0 .../pages/schema/schema-page.component.scss | 0 .../pages/schema/schema-page.component.ts | 0 .../schema-preview-urls-form.component.html | 0 .../schema-preview-urls-form.component.scss | 0 .../schema-preview-urls-form.component.ts | 0 .../schema/schema-scripts-form.component.html | 0 .../schema/schema-scripts-form.component.scss | 0 .../schema/schema-scripts-form.component.ts | 0 .../types/array-validation.component.html | 0 .../types/array-validation.component.scss | 0 .../types/array-validation.component.ts | 0 .../schema/types/assets-ui.component.html | 0 .../schema/types/assets-ui.component.scss | 0 .../pages/schema/types/assets-ui.component.ts | 0 .../types/assets-validation.component.html | 0 .../types/assets-validation.component.scss | 0 .../types/assets-validation.component.ts | 0 .../schema/types/boolean-ui.component.html | 0 .../schema/types/boolean-ui.component.scss | 0 .../schema/types/boolean-ui.component.ts | 0 .../types/boolean-validation.component.html | 0 .../types/boolean-validation.component.scss | 0 .../types/boolean-validation.component.ts | 0 .../schema/types/date-time-ui.component.html | 0 .../schema/types/date-time-ui.component.scss | 0 .../schema/types/date-time-ui.component.ts | 0 .../types/date-time-validation.component.html | 0 .../types/date-time-validation.component.scss | 0 .../types/date-time-validation.component.ts | 0 .../types/geolocation-ui.component.html | 0 .../types/geolocation-ui.component.scss | 0 .../schema/types/geolocation-ui.component.ts | 0 .../geolocation-validation.component.html | 0 .../geolocation-validation.component.scss | 0 .../types/geolocation-validation.component.ts | 0 .../pages/schema/types/json-ui.component.html | 0 .../pages/schema/types/json-ui.component.scss | 0 .../pages/schema/types/json-ui.component.ts | 0 .../types/json-validation.component.html | 0 .../types/json-validation.component.scss | 0 .../schema/types/json-validation.component.ts | 0 .../schema/types/number-ui.component.html | 0 .../schema/types/number-ui.component.scss | 0 .../pages/schema/types/number-ui.component.ts | 0 .../types/number-validation.component.html | 0 .../types/number-validation.component.scss | 0 .../types/number-validation.component.ts | 0 .../schema/types/references-ui.component.html | 0 .../schema/types/references-ui.component.scss | 0 .../schema/types/references-ui.component.ts | 0 .../references-validation.component.html | 0 .../references-validation.component.scss | 0 .../types/references-validation.component.ts | 0 .../schema/types/string-ui.component.html | 0 .../schema/types/string-ui.component.scss | 0 .../pages/schema/types/string-ui.component.ts | 0 .../types/string-validation.component.html | 0 .../types/string-validation.component.scss | 0 .../types/string-validation.component.ts | 0 .../pages/schema/types/tags-ui.component.html | 0 .../pages/schema/types/tags-ui.component.scss | 0 .../pages/schema/types/tags-ui.component.ts | 0 .../types/tags-validation.component.html | 0 .../types/tags-validation.component.scss | 0 .../schema/types/tags-validation.component.ts | 0 .../pages/schemas/schema-form.component.html | 0 .../pages/schemas/schema-form.component.scss | 0 .../pages/schemas/schema-form.component.ts | 0 .../pages/schemas/schemas-page.component.html | 0 .../pages/schemas/schemas-page.component.scss | 0 .../pages/schemas/schemas-page.component.ts | 0 .../app/features/settings/declarations.ts | 0 .../app/features/settings/index.ts | 0 .../app/features/settings/module.ts | 0 .../pages/backups/backup.component.ts | 0 .../pages/backups/backups-page.component.html | 0 .../pages/backups/backups-page.component.scss | 0 .../pages/backups/backups-page.component.ts | 0 .../clients/client-add-form.component.ts | 0 .../pages/clients/client.component.html | 0 .../pages/clients/client.component.scss | 0 .../pages/clients/client.component.ts | 0 .../pages/clients/clients-page.component.html | 0 .../pages/clients/clients-page.component.scss | 0 .../pages/clients/clients-page.component.ts | 0 .../contributor-add-form.component.html | 0 .../contributor-add-form.component.scss | 0 .../contributor-add-form.component.ts | 0 .../contributors/contributor.component.ts | 0 .../contributors-page.component.html | 0 .../contributors-page.component.scss | 0 .../contributors-page.component.ts | 0 .../import-contributors-dialog.component.html | 0 .../import-contributors-dialog.component.scss | 0 .../import-contributors-dialog.component.ts | 0 .../languages/language-add-form.component.ts | 0 .../pages/languages/language.component.html | 0 .../pages/languages/language.component.scss | 0 .../pages/languages/language.component.ts | 0 .../languages/languages-page.component.html | 0 .../languages/languages-page.component.scss | 0 .../languages/languages-page.component.ts | 0 .../pages/more/more-page.component.html | 0 .../pages/more/more-page.component.scss | 0 .../pages/more/more-page.component.ts | 0 .../pages/patterns/pattern.component.html | 0 .../pages/patterns/pattern.component.scss | 0 .../pages/patterns/pattern.component.ts | 0 .../patterns/patterns-page.component.html | 0 .../patterns/patterns-page.component.scss | 0 .../pages/patterns/patterns-page.component.ts | 0 .../settings/pages/plans/plan.component.html | 0 .../settings/pages/plans/plan.component.scss | 0 .../settings/pages/plans/plan.component.ts | 0 .../pages/plans/plans-page.component.html | 0 .../pages/plans/plans-page.component.scss | 0 .../pages/plans/plans-page.component.ts | 0 .../pages/roles/role-add-form.component.ts | 0 .../settings/pages/roles/role.component.html | 0 .../settings/pages/roles/role.component.scss | 0 .../settings/pages/roles/role.component.ts | 0 .../pages/roles/roles-page.component.html | 0 .../pages/roles/roles-page.component.scss | 0 .../pages/roles/roles-page.component.ts | 0 .../workflows/workflow-add-form.component.ts | 0 .../workflows/workflow-step.component.html | 0 .../workflows/workflow-step.component.scss | 0 .../workflows/workflow-step.component.ts | 0 .../workflow-transition.component.html | 0 .../workflow-transition.component.scss | 0 .../workflow-transition.component.ts | 0 .../pages/workflows/workflow.component.html | 0 .../pages/workflows/workflow.component.scss | 0 .../pages/workflows/workflow.component.ts | 0 .../workflows/workflows-page.component.html | 0 .../workflows/workflows-page.component.scss | 0 .../workflows/workflows-page.component.ts | 0 .../settings/settings-area.component.html | 0 .../settings/settings-area.component.scss | 0 .../settings/settings-area.component.ts | 0 .../app/framework/angular/animations.ts | 0 .../app/framework/angular/avatar.component.ts | 0 .../app/framework/angular/code.component.html | 0 .../app/framework/angular/code.component.scss | 0 .../app/framework/angular/code.component.ts | 0 .../app/framework/angular/drag-helper.ts | 0 .../angular/external-link.directive.ts | 0 .../angular/forms/autocomplete.component.html | 0 .../angular/forms/autocomplete.component.scss | 0 .../angular/forms/autocomplete.component.ts | 0 .../forms/checkbox-group.component.html | 0 .../forms/checkbox-group.component.scss | 0 .../angular/forms/checkbox-group.component.ts | 0 .../angular/forms/code-editor.component.html | 0 .../angular/forms/code-editor.component.scss | 0 .../angular/forms/code-editor.component.ts | 0 .../angular/forms/color-picker.component.html | 0 .../angular/forms/color-picker.component.scss | 0 .../angular/forms/color-picker.component.ts | 0 .../angular/forms/confirm-click.directive.ts | 0 .../forms/control-errors.component.html | 0 .../forms/control-errors.component.scss | 0 .../angular/forms/control-errors.component.ts | 0 .../framework/angular/forms/copy.directive.ts | 0 .../forms/date-time-editor.component.html | 0 .../forms/date-time-editor.component.scss | 0 .../forms/date-time-editor.component.ts | 0 .../angular/forms/dropdown.component.html | 0 .../angular/forms/dropdown.component.scss | 0 .../angular/forms/dropdown.component.ts | 0 .../forms/editable-title.component.html | 0 .../forms/editable-title.component.scss | 0 .../angular/forms/editable-title.component.ts | 0 .../angular/forms/error-formatting.spec.ts | 0 .../angular/forms/error-formatting.ts | 0 .../angular/forms/file-drop.directive.ts | 0 .../forms/focus-on-init.directive.spec.ts | 0 .../angular/forms/focus-on-init.directive.ts | 0 .../angular/forms/form-alert.component.ts | 0 .../angular/forms/form-error.component.ts | 0 .../angular/forms/form-hint.component.ts | 0 .../framework/angular/forms/forms-helper.ts | 0 .../forms/iframe-editor.component.html | 0 .../forms/iframe-editor.component.scss | 0 .../angular/forms/iframe-editor.component.ts | 0 .../forms/indeterminate-value.directive.ts | 0 .../angular/forms/json-editor.component.html | 0 .../angular/forms/json-editor.component.scss | 0 .../angular/forms/json-editor.component.ts | 0 .../angular/forms/progress-bar.component.ts | 0 .../angular/forms/stars.component.html | 0 .../angular/forms/stars.component.scss | 0 .../angular/forms/stars.component.ts | 0 .../angular/forms/tag-editor.component.html | 0 .../angular/forms/tag-editor.component.scss | 0 .../angular/forms/tag-editor.component.ts | 0 .../angular/forms/toggle.component.html | 0 .../angular/forms/toggle.component.scss | 0 .../angular/forms/toggle.component.ts | 0 .../forms/transform-input.directive.ts | 0 .../angular/forms/validators.spec.ts | 0 .../app/framework/angular/forms/validators.ts | 0 .../app/framework/angular/highlight.pipe.ts | 0 .../angular/hover-background.directive.ts | 0 .../angular/http/caching.interceptor.ts | 0 .../framework/angular/http/http-extensions.ts | 0 .../angular/http/loading.interceptor.ts | 0 .../angular/ignore-scrollbar.directive.ts | 0 .../angular/image-source.directive.ts | 0 .../modals/dialog-renderer.component.html | 0 .../modals/dialog-renderer.component.scss | 0 .../modals/dialog-renderer.component.ts | 0 .../modals/modal-dialog.component.html | 0 .../modals/modal-dialog.component.scss | 0 .../angular/modals/modal-dialog.component.ts | 0 .../modals/modal-placement.directive.ts | 0 .../angular/modals/modal.directive.ts | 0 .../modals/onboarding-tooltip.component.html | 0 .../modals/onboarding-tooltip.component.scss | 0 .../modals/onboarding-tooltip.component.ts | 0 .../angular/modals/root-view.component.ts | 0 .../angular/modals/tooltip.directive.ts | 0 .../framework/angular/pager.component.html | 0 .../framework/angular/pager.component.scss | 0 .../app/framework/angular/pager.component.ts | 0 .../angular/panel-container.directive.ts | 0 .../framework/angular/panel.component.html | 0 .../framework/angular/panel.component.scss | 0 .../app/framework/angular/panel.component.ts | 0 .../angular/pipes/colors.pipes.spec.ts | 0 .../framework/angular/pipes/colors.pipes.ts | 0 .../angular/pipes/date-time.pipes.spec.ts | 0 .../angular/pipes/date-time.pipes.ts | 0 .../framework/angular/pipes/keys.pipe.spec.ts | 0 .../app/framework/angular/pipes/keys.pipe.ts | 0 .../angular/pipes/markdown.pipe.spec.ts | 0 .../framework/angular/pipes/markdown.pipe.ts | 0 .../angular/pipes/money.pipe.spec.ts | 0 .../app/framework/angular/pipes/money.pipe.ts | 0 .../framework/angular/pipes/name.pipe.spec.ts | 0 .../app/framework/angular/pipes/name.pipe.ts | 0 .../angular/pipes/numbers.pipes.spec.ts | 0 .../framework/angular/pipes/numbers.pipes.ts | 0 .../framework/angular/popup-link.directive.ts | 0 .../routers/can-deactivate.guard.spec.ts | 0 .../angular/routers/can-deactivate.guard.ts | 0 .../angular/routers/parent-link.directive.ts | 0 .../angular/routers/router-utils.spec.ts | 0 .../framework/angular/routers/router-utils.ts | 0 .../app/framework/angular/safe-html.pipe.ts | 0 .../angular/scroll-active.directive.ts | 0 .../angular/shortcut.component.spec.ts | 0 .../framework/angular/shortcut.component.ts | 0 .../framework/angular/stateful.component.ts | 0 .../angular/status-icon.component.html | 0 .../angular/status-icon.component.scss | 0 .../angular/status-icon.component.ts | 0 .../framework/angular/stop-click.directive.ts | 0 .../angular/sync-scrolling.directive.ts | 0 .../angular/template-wrapper.directive.ts | 0 .../app/framework/angular/title.component.ts | 0 .../app/framework/configurations.ts | 0 .../app/framework/declarations.ts | 0 .../app/framework/index.ts | 0 .../app/framework/internal.ts | 0 .../app/framework/module.ts | 0 .../framework/services/analytics.service.ts | 0 .../services/clipboard.service.spec.ts | 0 .../framework/services/clipboard.service.ts | 0 .../framework/services/dialog.service.spec.ts | 0 .../app/framework/services/dialog.service.ts | 0 .../services/loading.service.spec.ts | 0 .../app/framework/services/loading.service.ts | 0 .../services/local-store.service.spec.ts | 0 .../framework/services/local-store.service.ts | 0 .../services/message-bus.service.spec.ts | 0 .../framework/services/message-bus.service.ts | 0 .../services/onboarding.service.spec.ts | 0 .../framework/services/onboarding.service.ts | 0 .../services/resource-loader.service.ts | 0 .../services/shortcut.service.spec.ts | 0 .../framework/services/shortcut.service.ts | 0 .../framework/services/title.service.spec.ts | 0 .../app/framework/services/title.service.ts | 0 .../app/framework/state.ts | 0 .../framework/utils/array-extensions.spec.ts | 0 .../app/framework/utils/array-extensions.ts | 0 .../app/framework/utils/array-helper.ts | 0 .../app/framework/utils/date-helper.spec.ts | 0 .../app/framework/utils/date-helper.ts | 0 .../app/framework/utils/date-time.spec.ts | 0 .../app/framework/utils/date-time.ts | 0 .../app/framework/utils/duration.spec.ts | 0 .../app/framework/utils/duration.ts | 0 .../app/framework/utils/error.spec.ts | 0 .../app/framework/utils/error.ts | 0 .../app/framework/utils/hateos.ts | 0 .../app/framework/utils/interpolator.spec.ts | 0 .../app/framework/utils/interpolator.ts | 0 .../app/framework/utils/keys.ts | 0 .../app/framework/utils/math-helper.spec.ts | 0 .../app/framework/utils/math-helper.ts | 0 .../framework/utils/modal-positioner.spec.ts | 0 .../app/framework/utils/modal-positioner.ts | 0 .../app/framework/utils/modal-view.spec.ts | 0 .../app/framework/utils/modal-view.ts | 0 .../app/framework/utils/pager.spec.ts | 0 .../app/framework/utils/pager.ts | 0 .../app/framework/utils/picasso.ts | 0 .../app/framework/utils/rxjs-extensions.ts | 0 .../app/framework/utils/string-helper.spec.ts | 0 .../app/framework/utils/string-helper.ts | 0 .../app/framework/utils/types.spec.ts | 0 .../app/framework/utils/types.ts | 0 .../app/framework/utils/version.spec.ts | 0 .../app/framework/utils/version.ts | 0 .../wwwroot => frontend/app}/index.html | 0 .../shared/components/app-form.component.html | 0 .../shared/components/app-form.component.scss | 0 .../shared/components/app-form.component.ts | 0 .../components/asset-dialog.component.html | 0 .../components/asset-dialog.component.scss | 0 .../components/asset-dialog.component.ts | 0 .../components/asset-uploader.component.html | 0 .../components/asset-uploader.component.scss | 0 .../components/asset-uploader.component.ts | 0 .../shared/components/asset.component.html | 0 .../shared/components/asset.component.scss | 0 .../app/shared/components/asset.component.ts | 0 .../components/assets-list.component.html | 0 .../components/assets-list.component.scss | 0 .../components/assets-list.component.ts | 0 .../components/assets-selector.component.html | 0 .../components/assets-selector.component.scss | 0 .../components/assets-selector.component.ts | 0 .../shared/components/comment.component.html | 0 .../shared/components/comment.component.scss | 0 .../shared/components/comment.component.ts | 0 .../shared/components/comments.component.html | 0 .../shared/components/comments.component.scss | 0 .../shared/components/comments.component.ts | 0 .../geolocation-editor.component.html | 0 .../geolocation-editor.component.scss | 0 .../geolocation-editor.component.ts | 0 .../components/help-markdown.pipe.spec.ts | 0 .../shared/components/help-markdown.pipe.ts | 0 .../app/shared/components/help.component.html | 0 .../app/shared/components/help.component.scss | 0 .../app/shared/components/help.component.ts | 0 .../components/history-list.component.html | 0 .../components/history-list.component.scss | 0 .../components/history-list.component.ts | 0 .../shared/components/history.component.html | 0 .../shared/components/history.component.scss | 0 .../shared/components/history.component.ts | 0 .../language-selector.component.html | 0 .../language-selector.component.scss | 0 .../components/language-selector.component.ts | 0 .../components/markdown-editor.component.html | 0 .../components/markdown-editor.component.scss | 0 .../components/markdown-editor.component.ts | 0 .../app/shared/components/pipes.ts | 0 .../queries/filter-comparison.component.html | 0 .../queries/filter-comparison.component.scss | 0 .../queries/filter-comparison.component.ts | 0 .../queries/filter-logical.component.html | 0 .../queries/filter-logical.component.scss | 0 .../queries/filter-logical.component.ts | 0 .../queries/filter-node.component.ts | 0 .../components/queries/query.component.ts | 0 .../components/queries/sorting.component.ts | 0 .../references-dropdown.component.ts | 0 .../components/rich-editor.component.html | 0 .../components/rich-editor.component.scss | 0 .../components/rich-editor.component.ts | 0 .../components/saved-queries.component.ts | 0 .../components/schema-category.component.html | 0 .../components/schema-category.component.scss | 0 .../components/schema-category.component.ts | 0 .../components/search-form.component.html | 0 .../components/search-form.component.scss | 0 .../components/search-form.component.ts | 0 .../components/table-header.component.ts | 0 .../app/shared/declarations.ts | 0 .../guards/app-must-exist.guard.spec.ts | 0 .../app/shared/guards/app-must-exist.guard.ts | 0 .../guards/content-must-exist.guard.spec.ts | 0 .../shared/guards/content-must-exist.guard.ts | 0 .../app/shared/guards/load-apps.guard.spec.ts | 0 .../app/shared/guards/load-apps.guard.ts | 0 .../guards/load-languages.guard.spec.ts | 0 .../app/shared/guards/load-languages.guard.ts | 0 .../must-be-authenticated.guard.spec.ts | 0 .../guards/must-be-authenticated.guard.ts | 0 .../must-be-not-authenticated.guard.spec.ts | 0 .../guards/must-be-not-authenticated.guard.ts | 0 .../schema-must-exist-published.guard.spec.ts | 0 .../schema-must-exist-published.guard.ts | 0 .../guards/schema-must-exist.guard.spec.ts | 0 .../shared/guards/schema-must-exist.guard.ts | 0 ...schema-must-not-be-singleton.guard.spec.ts | 0 .../schema-must-not-be-singleton.guard.ts | 0 .../app/shared/guards/unset-app.guard.spec.ts | 0 .../app/shared/guards/unset-app.guard.ts | 0 .../shared/guards/unset-content.guard.spec.ts | 0 .../app/shared/guards/unset-content.guard.ts | 0 {src/Squidex => frontend}/app/shared/index.ts | 0 .../interceptors/auth.interceptor.spec.ts | 0 .../shared/interceptors/auth.interceptor.ts | 0 .../app/shared/internal.ts | 0 .../Squidex => frontend}/app/shared/module.ts | 0 .../services/app-languages.service.spec.ts | 0 .../shared/services/app-languages.service.ts | 0 .../app/shared/services/apps.service.spec.ts | 0 .../app/shared/services/apps.service.ts | 0 .../shared/services/assets.service.spec.ts | 0 .../app/shared/services/assets.service.ts | 0 .../app/shared/services/auth.service.ts | 0 .../shared/services/autosave.service.spec.ts | 0 .../app/shared/services/autosave.service.ts | 0 .../shared/services/backups.service.spec.ts | 0 .../app/shared/services/backups.service.ts | 0 .../shared/services/clients.service.spec.ts | 0 .../app/shared/services/clients.service.ts | 0 .../shared/services/comments.service.spec.ts | 0 .../app/shared/services/comments.service.ts | 0 .../shared/services/contents.service.spec.ts | 0 .../app/shared/services/contents.service.ts | 0 .../services/contributors.service.spec.ts | 0 .../shared/services/contributors.service.ts | 0 .../shared/services/graphql.service.spec.ts | 0 .../app/shared/services/graphql.service.ts | 0 .../app/shared/services/help.service.spec.ts | 0 .../app/shared/services/help.service.ts | 0 .../shared/services/history.service.spec.ts | 0 .../app/shared/services/history.service.ts | 0 .../shared/services/languages.service.spec.ts | 0 .../app/shared/services/languages.service.ts | 0 .../app/shared/services/news.service.spec.ts | 0 .../app/shared/services/news.service.ts | 0 .../shared/services/patterns.service.spec.ts | 0 .../app/shared/services/patterns.service.ts | 0 .../app/shared/services/plans.service.spec.ts | 0 .../app/shared/services/plans.service.ts | 0 .../app/shared/services/roles.service.spec.ts | 0 .../app/shared/services/roles.service.ts | 0 .../app/shared/services/rules.service.spec.ts | 0 .../app/shared/services/rules.service.ts | 0 .../shared/services/schemas.service.spec.ts | 0 .../app/shared/services/schemas.service.ts | 0 .../app/shared/services/schemas.types.ts | 0 .../services/translations.service.spec.ts | 0 .../shared/services/translations.service.ts | 0 .../app/shared/services/ui.service.spec.ts | 0 .../app/shared/services/ui.service.ts | 0 .../shared/services/usages.service.spec.ts | 0 .../app/shared/services/usages.service.ts | 0 .../services/users-provider.service.spec.ts | 0 .../shared/services/users-provider.service.ts | 0 .../app/shared/services/users.service.spec.ts | 0 .../app/shared/services/users.service.ts | 0 .../shared/services/workflows.service.spec.ts | 0 .../app/shared/services/workflows.service.ts | 0 .../app/shared/state/_test-helpers.ts | 0 .../app/shared/state/apps.forms.ts | 0 .../app/shared/state/apps.state.spec.ts | 0 .../app/shared/state/apps.state.ts | 0 .../shared/state/asset-uploader.state.spec.ts | 0 .../app/shared/state/asset-uploader.state.ts | 0 .../app/shared/state/assets.forms.ts | 0 .../app/shared/state/assets.state.spec.ts | 0 .../app/shared/state/assets.state.ts | 0 .../app/shared/state/backups.forms.ts | 0 .../app/shared/state/backups.state.spec.ts | 0 .../app/shared/state/backups.state.ts | 0 .../app/shared/state/clients.forms.ts | 0 .../app/shared/state/clients.state.spec.ts | 0 .../app/shared/state/clients.state.ts | 0 .../app/shared/state/comments.form.ts | 0 .../app/shared/state/comments.state.spec.ts | 0 .../app/shared/state/comments.state.ts | 0 .../app/shared/state/contents.forms.spec.ts | 0 .../app/shared/state/contents.forms.ts | 0 .../app/shared/state/contents.state.ts | 0 .../app/shared/state/contributors.forms.ts | 0 .../shared/state/contributors.state.spec.ts | 0 .../app/shared/state/contributors.state.ts | 0 .../app/shared/state/languages.forms.ts | 0 .../app/shared/state/languages.state.spec.ts | 0 .../app/shared/state/languages.state.ts | 0 .../app/shared/state/patterns.forms.ts | 0 .../app/shared/state/patterns.state.spec.ts | 0 .../app/shared/state/patterns.state.ts | 0 .../app/shared/state/plans.state.spec.ts | 0 .../app/shared/state/plans.state.ts | 0 .../app/shared/state/queries.spec.ts | 0 .../app/shared/state/queries.ts | 0 .../app/shared/state/query.ts | 0 .../app/shared/state/roles.forms.ts | 0 .../app/shared/state/roles.state.spec.ts | 0 .../app/shared/state/roles.state.ts | 0 .../shared/state/rule-events.state.spec.ts | 0 .../app/shared/state/rule-events.state.ts | 0 .../app/shared/state/rules.state.spec.ts | 0 .../app/shared/state/rules.state.ts | 0 .../app/shared/state/schema-tag-converter.ts | 0 .../app/shared/state/schemas.forms.ts | 0 .../app/shared/state/schemas.state.spec.ts | 0 .../app/shared/state/schemas.state.ts | 0 .../app/shared/state/ui.state.spec.ts | 0 .../app/shared/state/ui.state.ts | 0 .../app/shared/state/workflows.forms.ts | 0 .../app/shared/state/workflows.state.spec.ts | 0 .../app/shared/state/workflows.state.ts | 0 .../app/shared/utils/messages.ts | 0 .../app/shell/declarations.ts | 0 {src/Squidex => frontend}/app/shell/index.ts | 0 {src/Squidex => frontend}/app/shell/module.ts | 0 .../shell/pages/app/app-area.component.html | 0 .../shell/pages/app/app-area.component.scss | 0 .../app/shell/pages/app/app-area.component.ts | 0 .../shell/pages/app/left-menu.component.html | 0 .../shell/pages/app/left-menu.component.scss | 0 .../shell/pages/app/left-menu.component.ts | 0 .../forbidden/forbidden-page.component.ts | 0 .../shell/pages/home/home-page.component.html | 0 .../shell/pages/home/home-page.component.scss | 0 .../shell/pages/home/home-page.component.ts | 0 .../pages/internal/apps-menu.component.html | 0 .../pages/internal/apps-menu.component.scss | 0 .../pages/internal/apps-menu.component.ts | 0 .../internal/internal-area.component.html | 0 .../internal/internal-area.component.scss | 0 .../pages/internal/internal-area.component.ts | 0 .../internal/profile-menu.component.html | 0 .../internal/profile-menu.component.scss | 0 .../pages/internal/profile-menu.component.ts | 0 .../shell/pages/login/login-page.component.ts | 0 .../pages/logout/logout-page.component.ts | 0 .../not-found/not-found-page.component.ts | 0 {src/Squidex => frontend}/app/shims.ts | 0 .../app/theme/_bootstrap-vars.scss | 0 .../app/theme/_bootstrap.scss | 0 .../app/theme/_common.scss | 0 .../app/theme/_forms.scss | 0 .../app/theme/_lists.scss | 0 .../app/theme/_mixins.scss | 0 .../app/theme/_panels.scss | 0 .../app/theme/_static.scss | 0 .../Squidex => frontend}/app/theme/_vars.scss | 0 .../app/theme/icomoon/demo-files/demo.css | 0 .../app/theme/icomoon/demo-files/demo.js | 0 .../app/theme/icomoon/demo.html | 0 .../app/theme/icomoon/fonts/icomoon.eot | Bin .../app/theme/icomoon/fonts/icomoon.svg | 0 .../app/theme/icomoon/fonts/icomoon.ttf | Bin .../app/theme/icomoon/fonts/icomoon.woff | Bin .../theme/icomoon/icons/action-Algolia.svg | 0 .../app/theme/icomoon/icons/action-Fastly.svg | 0 .../app/theme/icomoon/icons/activity.svg | 0 .../app/theme/icomoon/icons/add-app.svg | 0 .../app/theme/icomoon/icons/add.svg | 0 .../app/theme/icomoon/icons/api.svg | 0 .../app/theme/icomoon/icons/assets.svg | 0 .../app/theme/icomoon/icons/caret-bottom.svg | 0 .../app/theme/icomoon/icons/caret-top.svg | 0 .../icomoon/icons/check-circle-filled.svg | 0 .../app/theme/icomoon/icons/check-circle.svg | 0 .../app/theme/icomoon/icons/client.svg | 0 .../app/theme/icomoon/icons/close.svg | 0 .../app/theme/icomoon/icons/contents.svg | 0 .../theme/icomoon/icons/control-Checkbox.svg | 0 .../icomoon/icons/control-Checkboxes.svg | 0 .../app/theme/icomoon/icons/control-Date.svg | 0 .../theme/icomoon/icons/control-DateTime.svg | 0 .../theme/icomoon/icons/control-Dropdown.svg | 0 .../app/theme/icomoon/icons/control-Html.svg | 0 .../app/theme/icomoon/icons/control-Input.svg | 0 .../theme/icomoon/icons/control-Markdown.svg | 0 .../app/theme/icomoon/icons/control-Radio.svg | 0 .../theme/icomoon/icons/control-RichText.svg | 0 .../app/theme/icomoon/icons/control-Slug.svg | 0 .../app/theme/icomoon/icons/control-Tags.svg | 0 .../theme/icomoon/icons/control-TextArea.svg | 0 .../theme/icomoon/icons/control-Toggle.svg | 0 .../app/theme/icomoon/icons/copy.svg | 0 .../app/theme/icomoon/icons/dashboard-api.svg | 0 .../icomoon/icons/dashboard-feedback.svg | 0 .../theme/icomoon/icons/dashboard-github.svg | 0 .../theme/icomoon/icons/dashboard-schema.svg | 0 .../app/theme/icomoon/icons/dashboard.svg | 0 .../app/theme/icomoon/icons/delete-filled.svg | 0 .../app/theme/icomoon/icons/delete.svg | 0 .../theme/icomoon/icons/document-delete.svg | 0 .../theme/icomoon/icons/document-disable.svg | 0 .../app/theme/icomoon/icons/document-lock.svg | 0 .../theme/icomoon/icons/document-publish.svg | 0 .../icomoon/icons/document-unpublish.svg | 0 .../app/theme/icomoon/icons/drag.svg | 0 .../app/theme/icomoon/icons/fastly.svg | 0 .../app/theme/icomoon/icons/filter.svg | 0 .../app/theme/icomoon/icons/help.svg | 0 .../app/theme/icomoon/icons/hide-all.svg | 0 .../app/theme/icomoon/icons/hide.svg | 0 .../app/theme/icomoon/icons/json.svg | 0 .../app/theme/icomoon/icons/location.svg | 0 .../app/theme/icomoon/icons/logo.svg | 0 .../app/theme/icomoon/icons/media.svg | 0 .../app/theme/icomoon/icons/more.svg | 0 .../theme/icomoon/icons/multiple-content.svg | 0 .../app/theme/icomoon/icons/orleans.svg | 0 .../app/theme/icomoon/icons/pencil.svg | 0 .../app/theme/icomoon/icons/reference.svg | 0 .../app/theme/icomoon/icons/schemas.svg | 0 .../app/theme/icomoon/icons/search.svg | 0 .../app/theme/icomoon/icons/settings.svg | 0 .../app/theme/icomoon/icons/show-all.svg | 0 .../app/theme/icomoon/icons/show.svg | 0 .../theme/icomoon/icons/single-content.svg | 0 .../app/theme/icomoon/icons/type-Array.svg | 0 .../app/theme/icomoon/icons/type-Boolean.svg | 0 .../app/theme/icomoon/icons/type-DateTime.svg | 0 .../app/theme/icomoon/icons/type-Number.svg | 0 .../app/theme/icomoon/icons/type-String.svg | 0 .../app/theme/icomoon/icons/type-Tags.svg | 0 .../app/theme/icomoon/icons/user-o.svg | 0 .../app/theme/icomoon/icons/user.svg | 0 .../app/theme/icomoon/icons/webhooks.svg | 0 .../app/theme/icomoon/selection.json | 0 .../app/theme/icomoon/style.css | 0 .../Squidex => frontend}/app/theme/theme.scss | 0 {src/Squidex => frontend}/karma.conf.js | 0 .../karma.coverage.conf.js | 0 frontend/package-lock.json | 17012 +++++++++++++++ frontend/package.json | 105 + {src/Squidex => frontend}/tsconfig.json | 0 {src/Squidex => frontend}/tslint.json | 0 libs/Dockerfile | 58 - libs/docker-compose.yml | 29 + nuget.exe | Bin 5010552 -> 0 bytes .../Apps/AppClient.cs | 46 - .../Apps/AppClients.cs | 90 - .../Apps/AppContributors.cs | 45 - .../Apps/AppImage.cs | 34 - .../Apps/AppPattern.cs | 35 - .../Apps/AppPatterns.cs | 62 - .../Apps/AppPlan.cs | 40 - .../Apps/Json/JsonAppPattern.cs | 38 - .../Apps/Json/JsonLanguagesConfig.cs | 62 - .../Apps/LanguageConfig.cs | 62 - .../Apps/LanguagesConfig.cs | 178 - .../Apps/Role.cs | 76 - .../Apps/Roles.cs | 179 - .../Comments/Comment.cs | 38 - .../Contents/ContentData.cs | 93 - .../Contents/ContentFieldData.cs | 71 - .../Contents/IdContentData.cs | 55 - .../Json/ContentFieldDataConverter.cs | 65 - .../Contents/Json/JsonWorkflowTransition.cs | 54 - .../Contents/NamedContentData.cs | 54 - .../Contents/Status.cs | 62 - .../Contents/StatusConverter.cs | 36 - .../Contents/Workflow.cs | 125 - .../Contents/WorkflowStep.cs | 31 - .../Contents/WorkflowTransition.cs | 27 - .../Contents/Workflows.cs | 83 - .../InvariantPartitioning.cs | 73 - src/Squidex.Domain.Apps.Core.Model/Named.cs | 23 - .../Partitioning.cs | 56 - .../PartitioningExtensions.cs | 26 - .../Rules/Rule.cs | 116 - .../Triggers/ContentChangedTriggerSchemaV2.cs | 18 - .../Schemas/ArrayField.cs | 91 - .../Schemas/ArrayFieldProperties.cs | 38 - .../Schemas/AssetsFieldProperties.cs | 62 - .../Schemas/BooleanFieldProperties.cs | 38 - .../Schemas/DateTimeFieldProperties.cs | 44 - .../Schemas/FieldCollection.cs | 169 - .../Schemas/FieldProperties.cs | 34 - .../Schemas/Fields.cs | 236 - .../Schemas/GeolocationFieldProperties.cs | 34 - .../Schemas/Json/JsonFieldModel.cs | 57 - .../Schemas/Json/JsonSchemaModel.cs | 111 - .../Schemas/JsonFieldProperties.cs | 32 - .../Schemas/NamedElementPropertiesBase.cs | 16 - .../Schemas/NestedField.cs | 113 - .../Schemas/NestedField{T}.cs | 68 - .../Schemas/NumberFieldProperties.cs | 48 - .../Schemas/ReferencesFieldProperties.cs | 63 - .../Schemas/RootField.cs | 122 - .../Schemas/RootField{T}.cs | 68 - .../Schemas/Schema.cs | 201 - .../Schemas/StringFieldProperties.cs | 52 - .../Schemas/TagsFieldProperties.cs | 44 - .../Schemas/UIFieldProperties.cs | 34 - .../Squidex.Domain.Apps.Core.Model.csproj | 30 - .../ConvertContent/ContentConverter.cs | 150 - .../ConvertContent/ContentConverterFlat.cs | 74 - .../ConvertContent/FieldConverters.cs | 373 - .../EnrichContent/ContentEnricher.cs | 76 - .../EnrichContent/DefaultValueFactory.cs | 97 - .../SchemaSynchronizer.cs | 223 - .../ContentReferencesExtensions.cs | 150 - .../ExtractReferenceIds/ReferencesCleaner.cs | 106 - .../ReferencesExtensions.cs | 69 - .../ReferencesExtractor.cs | 116 - .../GenerateEdmSchema/EdmSchemaExtensions.cs | 69 - .../GenerateEdmSchema/EdmTypeVisitor.cs | 103 - .../GenerateJsonSchema/Builder.cs | 64 - .../ContentSchemaBuilder.cs | 43 - .../JsonSchemaExtensions.cs | 73 - .../GenerateJsonSchema/JsonTypeVisitor.cs | 151 - .../EnrichedEvents/EnrichedUserEventBase.cs | 21 - .../HandleRules/EventEnricher.cs | 78 - .../HandleRules/IRuleTriggerHandler.cs | 27 - .../HandleRules/Result.cs | 96 - .../HandleRules/RuleActionHandler.cs | 86 - .../HandleRules/RuleActionProperty.cs | 24 - .../HandleRules/RuleActionRegistration.cs | 24 - .../HandleRules/RuleEventFormatter.cs | 314 - .../HandleRules/RuleRegistry.cs | 189 - .../HandleRules/RuleService.cs | 202 - .../HandleRules/RuleTriggerHandler.cs | 63 - .../ContentWrapper/ContentDataObject.cs | 130 - .../ContentWrapper/ContentDataProperty.cs | 65 - .../ContentWrapper/ContentFieldObject.cs | 135 - .../ContentWrapper/ContentFieldProperty.cs | 56 - .../Scripting/ContentWrapper/JsonMapper.cs | 131 - .../Scripting/DefaultConverter.cs | 60 - .../Scripting/IScriptEngine.cs | 26 - .../Scripting/JintScriptEngine.cs | 312 - .../Scripting/JintUser.cs | 59 - .../Scripting/ScriptContext.cs | 30 - ...Squidex.Domain.Apps.Core.Operations.csproj | 32 - .../Tags/ITagService.cs | 30 - .../Tags/TagNormalizer.cs | 150 - .../ValidateContent/ContentValidator.cs | 108 - .../FieldBagValidatorsFactory.cs | 85 - .../FieldValueValidatorsFactory.cs | 191 - .../ValidateContent/JsonValueConverter.cs | 231 - .../ValidateContent/Undefined.cs | 24 - .../ValidateContent/ValidationContext.cs | 127 - .../Validators/AggregateValidator.cs | 33 - .../Validators/AllowedValuesValidator.cs | 42 - .../Validators/AssetsValidator.cs | 116 - .../Validators/CollectionItemValidator.cs | 50 - .../Validators/CollectionValidator.cs | 72 - .../Validators/FieldValidator.cs | 67 - .../ValidateContent/Validators/IValidator.cs | 19 - .../Validators/NoValueValidator.cs | 30 - .../Validators/ObjectValidator.cs | 77 - .../Validators/PatternValidator.cs | 58 - .../Validators/RangeValidator.cs | 62 - .../Validators/ReferencesValidator.cs | 47 - .../Validators/RequiredStringValidator.cs | 42 - .../Validators/RequiredValidator.cs | 25 - .../Validators/StringLengthValidator.cs | 62 - .../Validators/UniqueValidator.cs | 51 - .../Validators/UniqueValuesValidator.cs | 32 - .../Assets/MongoAssetRepository.cs | 148 - .../MongoAssetRepository_SnapshotStore.cs | 75 - .../Contents/MongoContentCollection.cs | 271 - .../Contents/MongoContentEntity.cs | 133 - .../Contents/MongoContentRepository.cs | 148 - .../MongoContentRepository_SnapshotStore.cs | 93 - .../Contents/Visitors/FilterFactory.cs | 139 - .../Rules/MongoRuleEventEntity.cs | 62 - .../Rules/MongoRuleEventRepository.cs | 136 - ...quidex.Domain.Apps.Entities.MongoDb.csproj | 31 - .../AppProvider.cs | 121 - .../Apps/AppCommandMiddleware.cs | 72 - .../Apps/AppGrain.cs | 510 - .../Apps/AppHistoryEventsCreator.cs | 161 - .../Apps/AppUISettings.cs | 67 - .../Apps/AppUISettingsGrain.cs | 115 - .../Apps/BackupApps.cs | 201 - .../Apps/Commands/AddPattern.cs | 27 - .../Apps/Commands/UpdateApp.cs | 16 - .../Apps/Commands/UpdatePattern.cs | 22 - .../Apps/DefaultAppLogStore.cs | 34 - .../Diagnostics/OrleansAppsHealthCheck.cs | 36 - .../Apps/Guards/GuardApp.cs | 84 - .../Apps/Guards/GuardAppClients.cs | 104 - .../Apps/Guards/GuardAppContributors.cs | 99 - .../Apps/Guards/GuardAppLanguages.cs | 102 - .../Apps/Guards/GuardAppPatterns.cs | 102 - .../Apps/Guards/GuardAppRoles.cs | 102 - .../Apps/Guards/GuardAppWorkflows.cs | 108 - .../Apps/IAppEntity.cs | 43 - .../Apps/IAppUISettings.cs | 24 - .../Apps/Indexes/AppsIndex.cs | 286 - .../Apps/Indexes/IAppsIndex.cs | 39 - .../Invitation/InviteUserCommandMiddleware.cs | 52 - .../Apps/RolePermissionsProvider.cs | 76 - .../Apps/Services/IAppLimitsPlan.cs | 28 - .../Apps/Services/IAppPlanBillingManager.cs | 22 - .../Apps/Services/IAppPlansProvider.cs | 28 - .../Implementations/ConfigAppLimitsPlan.cs | 33 - .../Implementations/ConfigAppPlansProvider.cs | 98 - .../NoopAppPlanBillingManager.cs | 31 - .../Apps/Services/RedirectToCheckoutResult.cs | 24 - .../Apps/State/AppState.cs | 252 - .../Apps/Templates/Builders/FieldBuilder.cs | 77 - .../Apps/Templates/Builders/SchemaBuilder.cs | 149 - .../Templates/Builders/StringFieldBuilder.cs | 59 - .../Assets/AssetChangedTriggerHandler.cs | 69 - .../Assets/AssetCommandMiddleware.cs | 175 - .../Assets/AssetGrain.cs | 183 - .../Assets/AssetUsageTracker.cs | 69 - .../Assets/BackupAssets.cs | 127 - .../Assets/Commands/UploadAssetCommand.cs | 20 - .../Assets/Guards/GuardAsset.cs | 56 - .../Assets/IAssetQueryService.cs | 23 - .../Assets/Queries/AssetEnricher.cs | 93 - .../Assets/Queries/AssetLoader.cs | 44 - .../Assets/Queries/AssetQueryParser.cs | 174 - .../Assets/Queries/AssetQueryService.cs | 97 - .../Assets/Queries/FilterTagTransformer.cs | 51 - .../Assets/Repositories/IAssetRepository.cs | 30 - .../Backup/BackupGrain.cs | 262 - .../Backup/BackupHandlerWithStore.cs | 54 - .../Backup/BackupReader.cs | 155 - .../Backup/BackupWriter.cs | 108 - .../Backup/GuidMapper.cs | 108 - .../Backup/Helpers/Downloader.cs | 87 - .../Backup/IRestoreGrain.cs | 22 - .../Backup/RestoreGrain.cs | 367 - .../Backup/State/RestoreStateJob.cs | 49 - .../Comments/CommentsGrain.cs | 126 - .../Comments/Guards/GuardComments.cs | 89 - .../Contents/ContentChangedTriggerHandler.cs | 133 - .../Contents/ContentCommandMiddleware.cs | 49 - .../Contents/ContentEntity.cs | 61 - .../Contents/ContentGrain.cs | 377 - .../Contents/ContentHistoryEventsCreator.cs | 74 - .../Contents/ContentOperationContext.cs | 143 - .../Contents/ContentSchedulerGrain.cs | 107 - .../Contents/DefaultWorkflowsValidator.cs | 57 - .../Contents/DynamicContentWorkflow.cs | 153 - .../Contents/GraphQL/CachingGraphQLService.cs | 114 - .../GraphQL/GraphQLExecutionContext.cs | 142 - .../Contents/GraphQL/GraphQLModel.cs | 180 - .../Contents/GraphQL/IGraphModel.cs | 38 - .../Contents/GraphQL/IGraphQLUrlGenerator.cs | 26 - .../Contents/GraphQL/Middlewares.cs | 61 - .../Contents/GraphQL/Types/AssetGraphType.cs | 194 - .../GraphQL/Types/ContentDataGraphType.cs | 91 - .../GraphQL/Types/ContentGraphType.cs | 144 - .../GraphQL/Types/ContentUnionGraphType.cs | 60 - .../Contents/GraphQL/Types/NestedGraphType.cs | 63 - .../GraphQL/Types/QueryGraphTypeVisitor.cs | 150 - .../GraphQL/Types/Utils/GuidGraphType2.cs | 55 - .../GraphQL/Types/Utils/InstantGraphType.cs | 41 - .../GraphQL/Types/Utils/JsonConverter.cs | 32 - .../GraphQL/Types/Utils/JsonGraphType.cs | 42 - .../Contents/GraphQL/Types/Utils/JsonValue.cs | 25 - .../Contents/Guards/GuardContent.cs | 136 - .../Contents/IContentEntity.cs | 35 - .../Contents/IEnrichedContentEntity.cs | 29 - .../Contents/Queries/ContentEnricher.cs | 377 - .../Contents/Queries/ContentLoader.cs | 44 - .../Contents/Queries/ContentQueryParser.cs | 205 - .../Contents/Queries/ContentQueryService.cs | 341 - .../Contents/Queries/FilterTagTransformer.cs | 71 - .../Contents/Queries/QueryExecutionContext.cs | 133 - .../Repositories/IContentRepository.cs | 36 - .../Contents/State/ContentState.cs | 146 - .../Contents/Text/GrainTextIndexer.cs | 117 - .../Contents/Text/ITextIndexer.cs | 19 - .../Contents/Text/IndexState.cs | 144 - .../Contents/Text/MultiLanguageAnalyzer.cs | 65 - .../Contents/Text/TextIndexContent.cs | 210 - .../Contents/Text/TextIndexerGrain.cs | 259 - src/Squidex.Domain.Apps.Entities/Context.cs | 71 - .../EntityMapper.cs | 82 - .../History/HistoryEvent.cs | 57 - .../History/HistoryEventsCreatorBase.cs | 66 - .../History/HistoryService.cs | 90 - .../History/IHistoryEventsCreator.cs | 20 - .../NotificationEmailEventConsumer.cs | 121 - .../Notifications/NotificationEmailSender.cs | 113 - .../History/ParsedHistoryEvent.cs | 70 - .../IAppProvider.cs | 36 - .../IEntityWithCacheDependencies.cs | 16 - src/Squidex.Domain.Apps.Entities/Q.cs | 68 - .../Rules/BackupRules.cs | 54 - .../Rules/Guards/GuardRule.cs | 106 - .../Rules/Guards/RuleTriggerValidator.cs | 104 - .../Rules/IRuleEventEntity.cs | 28 - .../Rules/Indexes/RulesIndex.cs | 118 - .../Rules/ManualTriggerHandler.cs | 34 - .../Rules/Queries/RuleEnricher.cs | 80 - .../Repositories/IRuleEventRepository.cs | 38 - .../Rules/RuleDequeuerGrain.cs | 163 - .../Rules/RuleEnqueuer.cs | 102 - .../Rules/RuleEntity.cs | 46 - .../Rules/RuleGrain.cs | 154 - .../UsageTrackerCommandMiddleware.cs | 61 - .../Rules/UsageTracking/UsageTrackerGrain.cs | 158 - .../UsageTracking/UsageTriggerHandler.cs | 38 - .../Schemas/BackupSchemas.cs | 54 - .../Schemas/Guards/GuardSchema.cs | 251 - .../Schemas/Guards/GuardSchemaField.cs | 167 - .../Schemas/Indexes/ISchemasIndex.cs | 24 - .../Schemas/Indexes/SchemasIndex.cs | 181 - .../Schemas/SchemaChangedTriggerHandler.cs | 74 - .../Schemas/SchemaGrain.cs | 417 - .../Schemas/SchemaHistoryEventsCreator.cs | 93 - .../Squidex.Domain.Apps.Entities.csproj | 40 - .../Tags/GrainTagService.cs | 75 - .../Tags/ITagGrain.cs | 31 - .../Tags/TagGrain.cs | 152 - .../Apps/AppPatternAdded.cs | 24 - .../Apps/AppPatternUpdated.cs | 24 - .../Assets/AssetAnnotated.cs | 22 - .../Assets/AssetCreated.cs | 36 - .../Schemas/FieldAdded.cs | 22 - .../Schemas/ParentFieldEvent.cs | 16 - .../Squidex.Domain.Apps.Events.csproj | 28 - src/Squidex.Domain.Users.MongoDb/MongoUser.cs | 99 - .../MongoUserStore.cs | 526 - .../Squidex.Domain.Users.MongoDb.csproj | 31 - .../AssetUserPictureStore.cs | 42 - .../DefaultUserResolver.cs | 80 - .../DefaultXmlRepository.cs | 53 - .../PwnedPasswordValidator.cs | 54 - .../Squidex.Domain.Users.csproj | 30 - .../UserManagerExtensions.cs | 286 - src/Squidex.Domain.Users/UserWithClaims.cs | 54 - .../Assets/AzureBlobAssetStore.cs | 142 - .../EventSourcing/CosmosDbEventStore.cs | 138 - .../CosmosDbEventStore_Reader.cs | 142 - .../CosmosDbEventStore_Writer.cs | 149 - .../EventSourcing/CosmosDbSubscription.cs | 151 - .../EventSourcing/FilterBuilder.cs | 156 - .../EventSourcing/FilterExtensions.cs | 62 - .../EventSourcing/StreamPosition.cs | 55 - .../Squidex.Infrastructure.Azure.csproj | 23 - .../Diagnostics/GetEventStoreHealthCheck.cs | 33 - .../EventSourcing/Formatter.cs | 78 - .../EventSourcing/GetEventStore.cs | 224 - .../GetEventStoreSubscription.cs | 81 - .../EventSourcing/ProjectionClient.cs | 143 - ...quidex.Infrastructure.GetEventStore.csproj | 25 - .../Assets/GoogleCloudAssetStore.cs | 112 - .../Squidex.Infrastructure.GoogleCloud.csproj | 26 - .../Assets/MongoGridFsAssetStore.cs | 131 - .../Diagnostics/MongoDBHealthCheck.cs | 37 - .../EventSourcing/MongoEventStore.cs | 66 - .../EventSourcing/MongoEventStore_Reader.cs | 210 - .../EventSourcing/MongoEventStore_Writer.cs | 144 - .../EventSourcing/StreamPosition.cs | 60 - .../MongoDb/BsonJsonConvention.cs | 58 - .../MongoDb/BsonJsonReader.cs | 107 - .../MongoDb/BsonJsonSerializer.cs | 60 - .../MongoDb/BsonJsonWriter.cs | 178 - .../MongoDb/JTokenSerializer.cs | 53 - .../MongoDb/MongoExtensions.cs | 216 - .../MongoDb/MongoRepositoryBase.cs | 101 - .../MongoDb/Queries/FilterBuilder.cs | 41 - .../MongoDb/Queries/FilterVisitor.cs | 92 - .../MongoDb/Queries/SortBuilder.cs | 54 - .../Squidex.Infrastructure.MongoDb.csproj | 28 - .../States/MongoSnapshotStore.cs | 80 - .../UsageTracking/MongoUsageRepository.cs | 105 - .../CQRS/Events/RabbitMqEventConsumer.cs | 104 - .../Squidex.Infrastructure.RabbitMq.csproj | 26 - .../Squidex.Infrastructure.Redis.csproj | 26 - .../Assets/AssetAlreadyExistsException.cs | 38 - .../Assets/AssetFile.cs | 42 - .../Assets/AssetNotFoundException.cs | 38 - .../Assets/AssetStoreExtensions.cs | 74 - .../Assets/FTPAssetStore.cs | 158 - .../Assets/FolderAssetStore.cs | 142 - .../Assets/HasherStream.cs | 96 - .../Assets/IAssetStore.cs | 26 - .../Assets/IAssetThumbnailGenerator.cs | 19 - .../Assets/ImageInfo.cs | 25 - .../ImageSharpAssetThumbnailGenerator.cs | 95 - .../Assets/MemoryAssetStore.cs | 113 - .../Assets/NoopAssetStore.cs | 42 - .../Caching/AsyncLocalCache.cs | 77 - .../Caching/CachingProviderBase.cs | 28 - .../Caching/ILocalCache.cs | 22 - .../Caching/LRUCache.cs | 103 - .../Caching/LRUCacheItem.cs | 18 - .../CollectionExtensions.cs | 244 - .../Collections/ArrayDictionary.cs | 21 - .../ArrayDictionary{TKey,TValue}.cs | 164 - .../Commands/CommandContext.cs | 53 - .../Commands/CustomCommandMiddlewareRunner.cs | 41 - .../Commands/DomainObjectGrain.cs | 74 - .../Commands/DomainObjectGrainBase.cs | 225 - .../Commands/DomainObjectGrainFormatter.cs | 36 - .../EnrichWithTimestampCommandMiddleware.cs | 35 - .../Commands/GrainCommandMiddleware.cs | 51 - .../Commands/IDomainObjectGrain.cs | 18 - .../Commands/InMemoryCommandBus.cs | 50 - .../Commands/LogCommandMiddleware.cs | 73 - .../Commands/LogSnapshotDomainObjectGrain.cs | 96 - .../Commands/ReadonlyCommandMiddleware.cs | 35 - .../DelegateDisposable.cs | 28 - .../DependencyInjectionExtensions.cs | 96 - .../Diagnostics/GCHealthCheck.cs | 45 - .../Diagnostics/OrleansHealthCheck.cs | 36 - src/Squidex.Infrastructure/DomainException.cs | 31 - .../DomainObjectException.cs | 53 - .../Email/SmtpEmailSender.cs | 42 - .../EventSourcing/CompoundEventConsumer.cs | 77 - .../DefaultEventDataFormatter.cs | 73 - .../EventSourcing/Envelope{T}.cs | 43 - .../EventSourcing/EventData.cs | 31 - .../Grains/EventConsumerGrain.cs | 305 - .../Grains/EventConsumerManagerGrain.cs | 118 - .../Grains/EventConsumerState.cs | 51 - .../Grains/OrleansEventNotifier.cs | 38 - .../EventSourcing/IEventDataFormatter.cs | 18 - .../EventSourcing/IEventStore.cs | 33 - .../EventSourcing/PollingSubscription.cs | 58 - .../EventSourcing/RetrySubscription.cs | 117 - .../EventSourcing/StoredEvent.cs | 34 - .../EventSourcing/StreamFilter.cs | 22 - src/Squidex.Infrastructure/Guard.cs | 219 - .../Http/DumpFormatter.cs | 107 - .../Json/IJsonSerializer.cs | 23 - .../Newtonsoft/ConverterContractResolver.cs | 100 - .../Json/Newtonsoft/InstantConverter.cs | 64 - .../Json/Newtonsoft/JsonClassConverter.cs | 51 - .../Json/Newtonsoft/JsonValueConverter.cs | 184 - .../Newtonsoft/NewtonsoftJsonSerializer.cs | 100 - .../Newtonsoft/TypeNameSerializationBinder.cs | 48 - .../Json/Objects/IJsonValue.cs | 18 - .../Json/Objects/JsonArray.cs | 96 - .../Json/Objects/JsonNull.cs | 55 - .../Json/Objects/JsonObject.cs | 135 - .../Json/Objects/JsonScalar.cs | 53 - .../Json/Objects/JsonValue.cs | 136 - src/Squidex.Infrastructure/Language.cs | 112 - .../LanguagesInitializer.cs | 39 - .../Log/Adapter/SemanticLogLoggerProvider.cs | 58 - .../Log/ApplicationInfoLogAppender.cs | 41 - .../Log/ConstantsLogWriter.cs | 28 - src/Squidex.Infrastructure/Log/FileChannel.cs | 51 - .../Log/IObjectWriter.cs | 35 - .../Log/Internal/ConsoleLogProcessor.cs | 107 - .../Log/JsonLogWriter.cs | 225 - .../Log/LockingLogStore.cs | 83 - src/Squidex.Infrastructure/Log/Profiler.cs | 74 - .../Log/ProfilerSession.cs | 58 - .../Log/ProfilerSpan.cs | 66 - src/Squidex.Infrastructure/Log/SemanticLog.cs | 98 - .../Log/SemanticLogExtensions.cs | 189 - .../Log/TimestampLogAppender.cs | 26 - .../Migrations/IMigrationPath.cs | 16 - .../Migrations/MigrationFailedException.cs | 46 - .../Migrations/Migrator.cs | 101 - src/Squidex.Infrastructure/NamedId.cs | 17 - src/Squidex.Infrastructure/NamedId{T}.cs | 101 - src/Squidex.Infrastructure/None.cs | 20 - .../Orleans/ActivationLimit.cs | 77 - .../Orleans/GrainBase.cs | 63 - .../Orleans/GrainBootstrap.cs | 55 - .../Orleans/GrainState.cs | 85 - .../Orleans/ILockGrain.cs | 19 - .../Orleans/Indexes/IUniqueNameIndexGrain.cs | 35 - .../Orleans/Indexes/IdsIndexGrain.cs | 62 - .../Orleans/Indexes/UniqueNameIndexGrain.cs | 136 - src/Squidex.Infrastructure/Orleans/J{T}.cs | 93 - .../Orleans/LocalCacheFilter.cs | 41 - .../Orleans/LockGrain.cs | 46 - .../Orleans/LoggingFilter.cs | 48 - .../Orleans/StreamReaderWrapper.cs | 88 - .../Plugins/PluginManager.cs | 124 - .../Queries/ClrFilter.cs | 87 - .../Queries/ClrValue.cs | 140 - .../Queries/CompareFilter.cs | 67 - .../Queries/FilterNode.cs | 14 - .../Queries/Json/FilterConverter.cs | 163 - .../Queries/Json/JsonFilterVisitor.cs | 84 - .../Queries/Json/PropertyPathValidator.cs | 47 - .../Queries/Json/QueryParser.cs | 73 - .../Queries/Json/ValueConverter.cs | 238 - .../Queries/LogicalFilter.cs | 38 - .../Queries/NegateFilter.cs | 31 - .../Queries/OData/ConstantWithTypeVisitor.cs | 178 - .../Queries/OData/EdmModelExtensions.cs | 61 - .../Queries/Optimizer.cs | 67 - .../Queries/PascalCasePathConverter.cs | 30 - .../Queries/PropertyPath.cs | 54 - src/Squidex.Infrastructure/Queries/Query.cs | 56 - .../Queries/SortNode.cs | 33 - .../Queries/TransformVisitor.cs | 29 - src/Squidex.Infrastructure/RefToken.cs | 87 - .../Reflection/IPropertyAccessor.cs | 16 - .../Reflection/PropertiesTypeAccessor.cs | 78 - .../Reflection/PropertyAccessor.cs | 76 - .../Reflection/SimpleCopier.cs | 84 - .../Reflection/SimpleMapper.cs | 186 - .../Reflection/TypeNameRegistry.cs | 163 - src/Squidex.Infrastructure/RetryWindow.cs | 48 - .../Security/Extensions.cs | 75 - .../Security/Permission.Part.cs | 84 - .../Security/Permission.cs | 118 - .../Security/PermissionSet.cs | 91 - .../Squidex.Infrastructure.csproj | 45 - .../States/DefaultStreamNameResolver.cs | 45 - src/Squidex.Infrastructure/States/IStore.cs | 27 - .../States/IStreamNameResolver.cs | 18 - .../States/InconsistentStateException.cs | 58 - .../States/Persistence.cs | 26 - .../States/Persistence{TSnapshot,TKey}.cs | 241 - src/Squidex.Infrastructure/States/Store.cs | 73 - .../StringExtensions.cs | 801 - .../Tasks/AsyncLocalCleaner.cs | 29 - src/Squidex.Infrastructure/Tasks/AsyncLock.cs | 73 - .../Tasks/AsyncLockPool.cs | 36 - .../Tasks/PartitionedActionBlock.cs | 98 - .../Tasks/SingleThreadedDispatcher.cs | 67 - .../Tasks/TaskExtensions.cs | 101 - .../Tasks/TaskHelper.cs | 36 - .../Timers/CompletionTimer.cs | 89 - .../Translations/DeepLTranslator.cs | 95 - .../Translations/DeepLTranslatorOptions.cs | 14 - .../Translations/ITranslator.cs | 17 - .../Translations/NoopTranslator.cs | 22 - .../Translations/Translation.cs | 25 - .../UsageTracking/BackgroundUsageTracker.cs | 198 - .../UsageTracking/CachingUsageTracker.cs | 71 - .../UsageTracking/IUsageTracker.cs | 24 - .../UsageTracking/StoredUsage.cs | 30 - .../Validation/Validate.cs | 62 - .../Validation/ValidationError.cs | 56 - .../Validation/ValidationException.cs | 107 - src/Squidex.Shared/Permissions.cs | 184 - src/Squidex.Shared/Squidex.Shared.csproj | 24 - src/Squidex.Shared/Users/ClientUser.cs | 54 - src/Squidex.Shared/Users/IUserResolver.cs | 23 - src/Squidex.Shared/Users/UserExtensions.cs | 113 - src/Squidex.Web/ApiController.cs | 68 - .../ApiExceptionFilterAttribute.cs | 117 - .../AssetRequestSizeLimitAttribute.cs | 44 - .../EnrichWithSchemaIdCommandMiddleware.cs | 102 - src/Squidex.Web/ContextProvider.cs | 45 - src/Squidex.Web/Deferred.cs | 42 - src/Squidex.Web/EntityCreatedDto.cs | 27 - src/Squidex.Web/ExposedValues.cs | 65 - src/Squidex.Web/Extensions.cs | 66 - src/Squidex.Web/FileCallbackResult.cs | 42 - .../Json/TypedJsonInheritanceConverter.cs | 95 - src/Squidex.Web/PermissionExtensions.cs | 64 - src/Squidex.Web/Pipeline/ApiCostsFilter.cs | 88 - src/Squidex.Web/Pipeline/AppResolver.cs | 113 - .../Pipeline/LocalCacheMiddleware.cs | 34 - src/Squidex.Web/Resource.cs | 61 - src/Squidex.Web/ResourceLink.cs | 25 - src/Squidex.Web/Services/UrlGenerator.cs | 78 - src/Squidex.Web/Squidex.Web.csproj | 25 - src/Squidex.Web/UrlHelperExtensions.cs | 46 - .../Api/Config/OpenApi/CommonProcessor.cs | 50 - .../Api/Config/OpenApi/OpenApiExtensions.cs | 19 - .../Api/Config/OpenApi/OpenApiServices.cs | 85 - .../Api/Config/OpenApi/ScopesProcessor.cs | 58 - .../OpenApi/XmlResponseTypesProcessor.cs | 54 - .../Api/Config/OpenApi/XmlTagProcessor.cs | 47 - .../Api/Controllers/Apps/AppsController.cs | 302 - .../Api/Controllers/Apps/Models/AppDto.cs | 244 - .../Controllers/Apps/Models/ContributorDto.cs | 76 - .../Apps/Models/UpdateWorkflowDto.cs | 53 - .../Controllers/Apps/Models/WorkflowDto.cs | 77 - .../Apps/Models/WorkflowStepDto.cs | 58 - .../Apps/Models/WorkflowTransitionDto.cs | 40 - .../Assets/AssetContentController.cs | 202 - .../Controllers/Assets/AssetsController.cs | 319 - .../Controllers/Backups/RestoreController.cs | 80 - .../Contents/ContentsController.cs | 457 - .../Controllers/Contents/Models/ContentDto.cs | 194 - .../Contents/Models/ContentsDto.cs | 81 - .../Controllers/Plans/AppPlansController.cs | 93 - .../Controllers/Plans/Models/AppPlansDto.cs | 53 - .../Plans/Models/PlanChangedDto.cs | 17 - .../Rules/Models/RuleActionProcessor.cs | 78 - .../Schemas/Models/FieldPropertiesDto.cs | 75 - .../Schemas/Models/UpdateFieldDto.cs | 26 - .../Schemas/Models/UpsertSchemaDto.cs | 98 - .../Controllers/Schemas/SchemasController.cs | 330 - .../Api/Controllers/Users/Models/UserDto.cs | 95 - .../Users/UserManagementController.cs | 129 - .../Api/Controllers/Users/UsersController.cs | 198 - src/Squidex/Areas/Api/Startup.cs | 25 - .../Frontend/Middlewares/WebpackMiddleware.cs | 75 - src/Squidex/Areas/Frontend/Startup.cs | 87 - .../Config/Cert/IdentityCert.pfx | Bin 2573 -> 0 bytes .../Config/Cert/IdentityCert.snk | Bin 596 -> 0 bytes .../Config/IdentityServerExtensions.cs | 78 - .../Config/IdentityServerServices.cs | 113 - .../IdentityServer/Config/LazyClientStore.cs | 232 - .../Controllers/Account/AccountController.cs | 433 - .../Controllers/Account/ConsentVM.cs | 16 - .../Controllers/Account/LoginVM.cs | 26 - .../Controllers/Error/ErrorController.cs | 63 - .../IdentityServer/Controllers/Extensions.cs | 47 - .../Controllers/Profile/ProfileController.cs | 224 - .../Controllers/Profile/ProfileVM.cs | 37 - src/Squidex/Areas/IdentityServer/Startup.cs | 41 - .../Areas/IdentityServer/Views/Extensions.cs | 44 - src/Squidex/Areas/OrleansDashboard/Startup.cs | 27 - .../Middlewares/PortalRedirectMiddleware.cs | 37 - src/Squidex/Areas/Portal/Startup.cs | 26 - .../Authentication/AuthenticationServices.cs | 39 - .../GithubAuthenticationServices.cs | 30 - .../GoogleAuthenticationServices.cs | 30 - .../Config/Authentication/GoogleHandler.cs | 55 - .../Authentication/IdentityServerServices.cs | 65 - .../MicrosoftAuthenticationServices.cs | 30 - .../Config/Authentication/MicrosoftHandler.cs | 37 - .../Config/Authentication/OidcServices.cs | 43 - src/Squidex/Config/Domain/AssetServices.cs | 98 - src/Squidex/Config/Domain/EntitiesServices.cs | 371 - .../Config/Domain/EventPublishersServices.cs | 68 - .../Config/Domain/EventStoreServices.cs | 102 - .../Config/Domain/InfrastructureServices.cs | 86 - .../Config/Domain/LoggingExtensions.cs | 43 - src/Squidex/Config/Domain/LoggingServices.cs | 67 - src/Squidex/Config/Domain/RuleServices.cs | 55 - .../Config/Domain/SerializationServices.cs | 124 - src/Squidex/Config/Domain/StoreServices.cs | 126 - .../Config/Domain/SubscriptionServices.cs | 35 - src/Squidex/Config/Logging.cs | 67 - src/Squidex/Config/Orleans/OrleansServices.cs | 132 - src/Squidex/Config/Startup/BackgroundHost.cs | 39 - src/Squidex/Config/Startup/InitializerHost.cs | 38 - .../Config/Startup/MigrationRebuilderHost.cs | 31 - src/Squidex/Config/Startup/MigratorHost.cs | 31 - .../Config/Startup/SafeHostedService.cs | 59 - src/Squidex/Config/Web/WebExtensions.cs | 121 - src/Squidex/Config/Web/WebServices.cs | 75 - src/Squidex/Dockerfile | 12 - src/Squidex/Pipeline/OpenApi/NSwagHelper.cs | 114 - .../Pipeline/Plugins/PluginExtensions.cs | 81 - src/Squidex/Pipeline/Plugins/PluginLoaders.cs | 73 - .../Pipeline/Robots/RobotsTxtMiddleware.cs | 46 - src/Squidex/Pipeline/Squid/SquidMiddleware.cs | 144 - src/Squidex/Program.cs | 50 - src/Squidex/Squidex.csproj | 157 - src/Squidex/WebStartup.cs | 150 - src/Squidex/app-config/webpack.config.js | 376 - src/Squidex/package-lock.json | 17154 ---------------- src/Squidex/package.json | 107 - .../Model/Apps/RoleTests.cs | 79 - .../Model/Apps/RolesTests.cs | 162 - .../Model/Contents/WorkflowJsonTests.cs | 40 - .../Model/Contents/WorkflowTests.cs | 147 - .../Model/PartitioningTests.cs | 85 - .../Model/Rules/RuleTests.cs | 169 - .../Model/Schemas/SchemaFieldTests.cs | 116 - .../ContentConversionFlatTests.cs | 148 - .../EnrichContent/ContentEnrichmentTests.cs | 198 - .../SchemaSynchronizerTests.cs | 607 - .../ReferenceExtractionTests.cs | 308 - .../HandleRules/RuleServiceTests.cs | 331 - .../Operations/Tags/TagNormalizerTests.cs | 134 - .../ValidateContent/ArrayFieldTests.cs | 125 - .../ValidateContent/AssetsFieldTests.cs | 321 - .../ValidateContent/ReferencesFieldTests.cs | 192 - .../ValidateContent/StringFieldTests.cs | 139 - .../ValidateContent/TagsFieldTests.cs | 159 - .../ValidateContent/UIFieldTests.cs | 129 - .../ValidationTestExtensions.cs | 86 - .../Squidex.Domain.Apps.Core.Tests.csproj | 33 - .../TestUtils.cs | 173 - .../Apps/AppCommandMiddlewareTests.cs | 102 - .../Apps/AppGrainTests.cs | 658 - .../Billing/NoopAppPlanBillingManagerTests.cs | 36 - .../Apps/Guards/GuardAppContributorsTests.cs | 223 - .../Apps/Guards/GuardAppRolesTests.cs | 165 - .../Apps/Guards/GuardAppTests.cs | 131 - .../Apps/Guards/GuardAppWorkflowTests.cs | 213 - .../Apps/Indexes/AppsIndexTests.cs | 387 - .../Assets/AssetChangedTriggerHandlerTests.cs | 149 - .../Assets/FileTypeTagGeneratorTests.cs | 56 - .../Assets/MongoDb/MongoDbQueryTests.cs | 236 - .../Assets/Queries/AssetLoaderTests.cs | 66 - .../Queries/FilterTagTransformerTests.cs | 61 - .../ContentChangedTriggerHandlerTests.cs | 235 - .../Contents/ContentGrainTests.cs | 592 - .../Contents/DefaultContentWorkflowTests.cs | 139 - .../DefaultWorkflowsValidatorTests.cs | 113 - .../Contents/DynamicContentWorkflowTests.cs | 352 - .../Contents/GraphQL/GraphQLQueriesTests.cs | 1270 -- .../Contents/GraphQL/GraphQLTestBase.cs | 286 - .../Contents/MongoDb/MongoDbQueryTests.cs | 289 - .../Contents/Queries/ContentEnricherTests.cs | 204 - .../Contents/Queries/ContentLoaderTests.cs | 77 - .../Queries/ContentQueryServiceTests.cs | 503 - .../Queries/FilterTagTransformerTests.cs | 105 - .../Contents/Text/TextIndexerGrainTests.cs | 263 - .../NotificationEmailEventConsumerTests.cs | 191 - .../Rules/Guards/GuardRuleTests.cs | 188 - .../Triggers/ContentChangedTriggerTests.cs | 108 - .../Rules/ManualTriggerHandlerTests.cs | 39 - .../Rules/RuleEnqueuerTests.cs | 118 - .../UsageTracking/UsageTriggerHandlerTests.cs | 68 - .../Schemas/Guards/GuardSchemaFieldTests.cs | 379 - .../Schemas/Guards/GuardSchemaTests.cs | 530 - .../Schemas/Indexes/SchemasIndexTests.cs | 248 - .../SchemaChangedTriggerHandlerTests.cs | 146 - .../Squidex.Domain.Apps.Entities.Tests.csproj | 40 - .../TestHelpers/AExtensions.cs | 30 - .../TestHelpers/JsonHelper.cs | 68 - .../TestHelpers/Mocks.cs | 77 - .../DefaultUserResolverTests.cs | 112 - .../Squidex.Domain.Users.Tests.csproj | 33 - .../Assets/AssetStoreTests.cs | 164 - .../ImageSharpAssetThumbnailGeneratorTests.cs | 92 - .../CollectionExtensionsTests.cs | 278 - .../DomainObjectGrainFormatterTests.cs | 66 - .../Commands/DomainObjectGrainTests.cs | 220 - .../LogSnapshotDomainObjectGrainTests.cs | 280 - .../EventSourcing/EventStoreTests.cs | 376 - .../Grains/EventConsumerGrainTests.cs | 409 - .../Grains/EventConsumerManagerGrainTests.cs | 186 - .../EventSourcing/RetrySubscriptionTests.cs | 124 - .../GuardTests.cs | 367 - .../Http/DumpFormatterTests.cs | 131 - .../Json/ClaimsPrincipalConverterTests.cs | 55 - .../Json/Objects/JsonObjectTests.cs | 357 - .../LanguageTests.cs | 141 - .../LanguagesInitializerTests.cs | 61 - .../Log/LockingLogStoreTests.cs | 87 - .../Log/SemanticLogTests.cs | 525 - .../Migrations/MigratorTests.cs | 167 - .../MongoDb/MongoExtensionsTests.cs | 169 - .../NamedIdTests.cs | 140 - .../Indexes/UniqueNameIndexGrainTests.cs | 197 - .../Orleans/JsonExternalSerializerTests.cs | 118 - .../Orleans/LockGrainTests.cs | 50 - .../Queries/JsonQueryConversionTests.cs | 382 - .../Queries/PascalCasePathConverterTests.cs | 32 - .../Queries/QueryJsonConversionTests.cs | 374 - .../Queries/QueryODataConversionTests.cs | 424 - .../Queries/QueryOptimizationTests.cs | 94 - .../RefTokenTests.cs | 122 - .../Reflection/SimpleMapperTests.cs | 177 - .../Security/ExtensionsTests.cs | 68 - .../Squidex.Infrastructure.Tests.csproj | 46 - .../States/InconsistentStateExceptionTests.cs | 31 - .../TestHelpers/JsonHelper.cs | 68 - .../TestHelpers/MyDomainObject.cs | 80 - .../TestHelpers/MyGrain.cs | 29 - .../BackgroundUsageTrackerTests.cs | 228 - .../ValidationExceptionTests.cs | 81 - .../ApiExceptionFilterAttributeTests.cs | 123 - .../ApiPermissionAttributeTests.cs | 113 - .../ETagCommandMiddlewareTests.cs | 119 - .../EnrichWithActorCommandMiddlewareTests.cs | 114 - .../EnrichWithAppIdCommandMiddlewareTests.cs | 104 - ...nrichWithSchemaIdCommandMiddlewareTests.cs | 157 - .../Pipeline/ApiCostsFilterTests.cs | 167 - .../Pipeline/AppResolverTests.cs | 197 - .../Pipeline/ETagFilterTests.cs | 102 - .../Squidex.Web.Tests.csproj | 33 - .../GenerateLanguages.csproj | 16 - tools/LoadTest/LoadTest.csproj | 23 - tools/Migrate_00/Migrate_00.csproj | 19 - tools/Migrate_01/Migrate_01.csproj | 24 - tools/Migrate_01/MigrationPath.cs | 132 - tools/Migrate_01/Migrations/AddPatterns.cs | 60 - .../Migrations/ConvertEventStore.cs | 69 - .../Migrations/ConvertEventStoreAppId.cs | 97 - tools/Migrate_01/RebuildRunner.cs | 66 - 3442 files changed, 94228 insertions(+), 93724 deletions(-) delete mode 100644 Dockerfile.build create mode 100644 backend/.editorconfig rename NuGet.Config => backend/NuGet.Config (100%) rename Squidex.ruleset => backend/Squidex.ruleset (100%) rename Squidex.sln => backend/Squidex.sln (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs (100%) create mode 100644 backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/ClientPool.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Email/EmailAction.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Email/EmailActionHandler.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Email/EmailPlugin.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Fastly/FastlyAction.cs (100%) create mode 100644 backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/HttpHelper.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Kafka/KafkaAction.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Medium/MediumAction.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Medium/MediumPlugin.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Slack/SlackAction.cs (100%) create mode 100644 backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Slack/SlackPlugin.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Twitter/TweetAction.cs (100%) create mode 100644 backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Webhook/WebhookAction.cs (100%) create mode 100644 backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs rename {extensions => backend/extensions}/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs (100%) rename {extensions => backend/extensions}/Squidex.Extensions/Samples/Controllers/PluginController.cs (100%) create mode 100644 backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionConverter.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Freezable.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Named.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/IFieldSettings.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldNormalization.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj rename {src => backend/src}/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/ConvertContent/IAssetUrlGenerator.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/Ids.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/Constants.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEventType.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedManualEvent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventBase.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventType.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/IEnrichedEntityEvent.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleOptions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs rename {src => backend/src}/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/Adapt.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj create mode 100644 backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/ArchiveApp.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/IAppGrain.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Services/IChangePlanResult.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Services/PlanResetResult.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Templates/AlwaysCreateClientCommandMiddleware.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/AssetFieldBuilder.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/BooleanFieldBuilder.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/DateTimeFieldBuilder.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/JsonFieldBuilder.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/NumberFieldBuilder.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfileCommandMiddleware.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Apps/Templates/DefaultScripts.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/BackupHandler.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/BackupRestoreException.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/BackupVersion.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/Helpers/Safe.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/IRestoreJob.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/JobStatus.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/State/RestoreState.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Comments/ICommentsGrain.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Commands/DiscardChanges.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValue.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValueNode.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexState.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Text/Scope.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexContent.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Contents/Text/Update.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Context.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/DomainObjectState.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/EntityExtensions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/History/IHistoryService.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/History/Notifications/INotificationEmailSender.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/History/Notifications/NoopNotificationEmailSender.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailEventConsumer.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailSender.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailTextOptions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/IAppCommand.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/IContextProvider.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/IEntity.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/IEntityWithTags.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/ISchemaCommand.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Q.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/IRuleGrain.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/Repositories/RuleStatistics.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardHelper.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/ISchemaGrain.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj rename {src => backend/src}/Squidex.Domain.Apps.Entities/SquidexCommand.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/SquidexEntities.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Entities/SquidexEventEnricher.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs rename {src => backend/src}/Squidex.Domain.Apps.Entities/Tags/ITagGenerator.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs rename {src => backend/src}/Squidex.Domain.Apps.Events/AppEvent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/AppUsageExceeded.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppArchived.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppClientAttached.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppClientRevoked.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppCreated.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppImageRemoved.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppImageUploaded.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppLanguageAdded.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppLanguageRemoved.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppMasterLanguageSet.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppPlanReset.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppUpdated.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs rename {src => backend/src}/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Assets/AssetEvent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Contents/ContentChangesDiscarded.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Contents/ContentChangesPublished.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Contents/ContentEvent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Contents/ContentUpdateProposed.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/SchemaEvent.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs (100%) create mode 100644 backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj rename {src => backend/src}/Squidex.Domain.Apps.Events/SquidexEvent.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/SquidexEvents.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs (100%) rename {src => backend/src}/Squidex.Domain.Apps.Events/SquidexHeaders.cs (100%) rename {src => backend/src}/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs (100%) rename {src => backend/src}/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs (100%) create mode 100644 backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs create mode 100644 backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs create mode 100644 backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj create mode 100644 backend/src/Squidex.Domain.Users/AssetUserPictureStore.cs create mode 100644 backend/src/Squidex.Domain.Users/DefaultUserResolver.cs create mode 100644 backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs rename {src => backend/src}/Squidex.Domain.Users/IUserEvents.cs (100%) rename {src => backend/src}/Squidex.Domain.Users/IUserFactory.cs (100%) rename {src => backend/src}/Squidex.Domain.Users/IUserPictureStore.cs (100%) rename {src => backend/src}/Squidex.Domain.Users/NoopUserEvents.cs (100%) create mode 100644 backend/src/Squidex.Domain.Users/PwnedPasswordValidator.cs create mode 100644 backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj rename {src => backend/src}/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs (100%) create mode 100644 backend/src/Squidex.Domain.Users/UserManagerExtensions.cs rename {src => backend/src}/Squidex.Domain.Users/UserValues.cs (100%) create mode 100644 backend/src/Squidex.Domain.Users/UserWithClaims.cs create mode 100644 backend/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs rename {src => backend/src}/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs (100%) rename {src => backend/src}/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs (100%) rename {src => backend/src}/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs (100%) rename {src => backend/src}/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs create mode 100644 backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs create mode 100644 backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs create mode 100644 backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs create mode 100644 backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs create mode 100644 backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs create mode 100644 backend/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs create mode 100644 backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj create mode 100644 backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs create mode 100644 backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs create mode 100644 backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs create mode 100644 backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs create mode 100644 backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs create mode 100644 backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj create mode 100644 backend/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs create mode 100644 backend/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs rename {src => backend/src}/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs (100%) rename {src => backend/src}/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventCommit.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs rename {src => backend/src}/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs (100%) rename {src => backend/src}/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs (100%) rename {src => backend/src}/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs (100%) rename {src => backend/src}/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs (100%) rename {src => backend/src}/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs rename {src => backend/src}/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs (100%) rename {src => backend/src}/Squidex.Infrastructure.MongoDb/MongoDb/IVersionedEntity.cs (100%) rename {src => backend/src}/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs rename {src => backend/src}/Squidex.Infrastructure.MongoDb/MongoDb/MongoEntity.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs rename {src => backend/src}/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs rename {src => backend/src}/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs rename {src => backend/src}/Squidex.Infrastructure.MongoDb/States/MongoState.cs (100%) rename {src => backend/src}/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs create mode 100644 backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs create mode 100644 backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj rename {src => backend/src}/Squidex.Infrastructure.Redis/RedisPubSub.cs (100%) rename {src => backend/src}/Squidex.Infrastructure.Redis/RedisSubscription.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj create mode 100644 backend/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs create mode 100644 backend/src/Squidex.Infrastructure/Assets/AssetFile.cs create mode 100644 backend/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs create mode 100644 backend/src/Squidex.Infrastructure/Assets/AssetStoreExtensions.cs create mode 100644 backend/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs create mode 100644 backend/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs create mode 100644 backend/src/Squidex.Infrastructure/Assets/HasherStream.cs create mode 100644 backend/src/Squidex.Infrastructure/Assets/IAssetStore.cs create mode 100644 backend/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs create mode 100644 backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs create mode 100644 backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs create mode 100644 backend/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs create mode 100644 backend/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs create mode 100644 backend/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs create mode 100644 backend/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs create mode 100644 backend/src/Squidex.Infrastructure/Caching/ILocalCache.cs create mode 100644 backend/src/Squidex.Infrastructure/Caching/LRUCache.cs create mode 100644 backend/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs rename {src => backend/src}/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Cloneable.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Cloneable{T}.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/CollectionExtensions.cs create mode 100644 backend/src/Squidex.Infrastructure/Collections/ArrayDictionary.cs create mode 100644 backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs rename {src => backend/src}/Squidex.Infrastructure/Collections/ReadOnlyCollection.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Commands/CommandContext.cs rename {src => backend/src}/Squidex.Infrastructure/Commands/CommandExtensions.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs create mode 100644 backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs create mode 100644 backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs create mode 100644 backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs create mode 100644 backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs rename {src => backend/src}/Squidex.Infrastructure/Commands/EntityCreatedResult.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Commands/EntitySavedResult.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs rename {src => backend/src}/Squidex.Infrastructure/Commands/IAggregateCommand.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Commands/ICommand.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Commands/ICommandBus.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Commands/ICommandMiddleware.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Commands/ICustomCommandMiddleware.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs rename {src => backend/src}/Squidex.Infrastructure/Commands/IDomainState.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Commands/ITimestampCommand.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs create mode 100644 backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs create mode 100644 backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs create mode 100644 backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs rename {src => backend/src}/Squidex.Infrastructure/Commands/ReadonlyOptions.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Configuration/Alternatives.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/ConfigurationException.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/DelegateDisposable.cs create mode 100644 backend/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs create mode 100644 backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs rename {src => backend/src}/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs rename {src => backend/src}/Squidex.Infrastructure/DisposableObjectBase.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/DomainException.cs rename {src => backend/src}/Squidex.Infrastructure/DomainForbiddenException.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/DomainObjectDeletedException.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/DomainObjectException.cs rename {src => backend/src}/Squidex.Infrastructure/DomainObjectNotFoundException.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/DomainObjectVersionException.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Email/IEmailSender.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Email/SmptOptions.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs rename {src => backend/src}/Squidex.Infrastructure/EtagVersion.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/CompoundEventConsumer.cs create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/Envelope.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/EnvelopeHeaders.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/Grains/WrapperSubscription.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/IEvent.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/IEventDataFormatter.cs rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/IEventEnricher.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/NoopEvent.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs rename {src => backend/src}/Squidex.Infrastructure/EventSourcing/WrongEventVersionException.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/ExceptionHelper.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/FileExtensions.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/GravatarHelper.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Guard.cs rename {src => backend/src}/Squidex.Infrastructure/HashSet.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs rename {src => backend/src}/Squidex.Infrastructure/IBackgroundProcess.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/IFreezable.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/IInitializable.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/IResultList.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/InstantExtensions.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs rename {src => backend/src}/Squidex.Infrastructure/Json/Newtonsoft/ClaimsPrincipalConverter.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs rename {src => backend/src}/Squidex.Infrastructure/Json/Newtonsoft/EnvelopeHeadersConverter.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Json/Newtonsoft/ISupportedTypes.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Json/Newtonsoft/InstantConverter.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs rename {src => backend/src}/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs rename {src => backend/src}/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs rename {src => backend/src}/Squidex.Infrastructure/Json/Objects/JsonBoolean.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs rename {src => backend/src}/Squidex.Infrastructure/Json/Objects/JsonNumber.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs rename {src => backend/src}/Squidex.Infrastructure/Json/Objects/JsonString.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs rename {src => backend/src}/Squidex.Infrastructure/Json/Objects/JsonValueType.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Language.cs rename {src => backend/src}/Squidex.Infrastructure/Languages.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/LanguagesInitializer.cs rename {src => backend/src}/Squidex.Infrastructure/LanguagesOptions.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Log/Adapter/SemanticLogLogger.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs create mode 100644 backend/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs rename {src => backend/src}/Squidex.Infrastructure/Log/ConsoleLogChannel.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs rename {src => backend/src}/Squidex.Infrastructure/Log/DebugLogChannel.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Log/FileChannel.cs rename {src => backend/src}/Squidex.Infrastructure/Log/IArrayWriter.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Log/ILogAppender.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Log/ILogChannel.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Log/ILogStore.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Log/IObjectWriter.cs rename {src => backend/src}/Squidex.Infrastructure/Log/IObjectWriterFactory.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Log/ISemanticLog.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs rename {src => backend/src}/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Log/Internal/IConsole.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Log/JsonLogWriter.cs rename {src => backend/src}/Squidex.Infrastructure/Log/JsonLogWriterFactory.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Log/LockingLogStore.cs rename {src => backend/src}/Squidex.Infrastructure/Log/NoopDisposable.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Log/NoopLogStore.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Log/Profiler.cs create mode 100644 backend/src/Squidex.Infrastructure/Log/ProfilerSession.cs create mode 100644 backend/src/Squidex.Infrastructure/Log/ProfilerSpan.cs create mode 100644 backend/src/Squidex.Infrastructure/Log/SemanticLog.cs create mode 100644 backend/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs rename {src => backend/src}/Squidex.Infrastructure/Log/SemanticLogLevel.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs rename {src => backend/src}/Squidex.Infrastructure/Migrations/IMigrated.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Migrations/IMigration.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs rename {src => backend/src}/Squidex.Infrastructure/Migrations/IMigrationStatus.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs create mode 100644 backend/src/Squidex.Infrastructure/Migrations/Migrator.cs create mode 100644 backend/src/Squidex.Infrastructure/NamedId.cs create mode 100644 backend/src/Squidex.Infrastructure/NamedId{T}.cs rename {src => backend/src}/Squidex.Infrastructure/Net/IPAddressComparer.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/None.cs create mode 100644 backend/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs rename {src => backend/src}/Squidex.Infrastructure/Orleans/ActivationLimiter.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Orleans/GrainBase.cs create mode 100644 backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs rename {src => backend/src}/Squidex.Infrastructure/Orleans/GrainOfGuid.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Orleans/GrainOfString.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Orleans/GrainState.cs rename {src => backend/src}/Squidex.Infrastructure/Orleans/IActivationLimit.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Orleans/IActivationLimiter.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Orleans/IDeactivater.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Orleans/IGrainState.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Orleans/ILockGrain.cs rename {src => backend/src}/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs create mode 100644 backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs rename {src => backend/src}/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs rename {src => backend/src}/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Orleans/J.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Orleans/J{T}.cs create mode 100644 backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs create mode 100644 backend/src/Squidex.Infrastructure/Orleans/LockGrain.cs create mode 100644 backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs rename {src => backend/src}/Squidex.Infrastructure/Orleans/SingleGrain.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Orleans/StateFilter.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs rename {src => backend/src}/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Plugins/IPlugin.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Plugins/IWebPlugin.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs rename {src => backend/src}/Squidex.Infrastructure/Plugins/PluginOptions.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs rename {src => backend/src}/Squidex.Infrastructure/Queries/ClrQuery.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Queries/ClrValue.cs rename {src => backend/src}/Squidex.Infrastructure/Queries/ClrValueType.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs rename {src => backend/src}/Squidex.Infrastructure/Queries/CompareOperator.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Queries/FilterNode.cs rename {src => backend/src}/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs create mode 100644 backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs rename {src => backend/src}/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs create mode 100644 backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs create mode 100644 backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs create mode 100644 backend/src/Squidex.Infrastructure/Queries/LogicalFilter.cs rename {src => backend/src}/Squidex.Infrastructure/Queries/LogicalFilterType.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Queries/NegateFilter.cs rename {src => backend/src}/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs create mode 100644 backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs rename {src => backend/src}/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Queries/OData/SortBuilder.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Queries/Optimizer.cs create mode 100644 backend/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs create mode 100644 backend/src/Squidex.Infrastructure/Queries/PropertyPath.cs create mode 100644 backend/src/Squidex.Infrastructure/Queries/Query.cs rename {src => backend/src}/Squidex.Infrastructure/Queries/SortBuilder.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Queries/SortNode.cs rename {src => backend/src}/Squidex.Infrastructure/Queries/SortOrder.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Queries/TransformVisitor.cs rename {src => backend/src}/Squidex.Infrastructure/RandomHash.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/RefToken.cs rename {src => backend/src}/Squidex.Infrastructure/RefTokenType.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs rename {src => backend/src}/Squidex.Infrastructure/Reflection/ITypeProvider.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs create mode 100644 backend/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs rename {src => backend/src}/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs create mode 100644 backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs rename {src => backend/src}/Squidex.Infrastructure/Reflection/TypeNameAttribute.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs rename {src => backend/src}/Squidex.Infrastructure/ResultList.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/RetryWindow.cs create mode 100644 backend/src/Squidex.Infrastructure/Security/Extensions.cs rename {src => backend/src}/Squidex.Infrastructure/Security/OpenIdClaims.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Security/Permission.Part.cs create mode 100644 backend/src/Squidex.Infrastructure/Security/Permission.cs create mode 100644 backend/src/Squidex.Infrastructure/Security/PermissionSet.cs rename {src => backend/src}/Squidex.Infrastructure/Singletons.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj rename {src => backend/src}/Squidex.Infrastructure/SquidexInfrastructure.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/States/CollectionNameAttribute.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs rename {src => backend/src}/Squidex.Infrastructure/States/IPersistence.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/States/IPersistence{TState}.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/States/ISnapshotStore.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/States/IStore.cs create mode 100644 backend/src/Squidex.Infrastructure/States/IStreamNameResolver.cs create mode 100644 backend/src/Squidex.Infrastructure/States/InconsistentStateException.cs create mode 100644 backend/src/Squidex.Infrastructure/States/Persistence.cs rename {src => backend/src}/Squidex.Infrastructure/States/PersistenceMode.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs create mode 100644 backend/src/Squidex.Infrastructure/States/Store.cs rename {src => backend/src}/Squidex.Infrastructure/States/StoreExtensions.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/StringExtensions.cs create mode 100644 backend/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs create mode 100644 backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs create mode 100644 backend/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs create mode 100644 backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs create mode 100644 backend/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs create mode 100644 backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs create mode 100644 backend/src/Squidex.Infrastructure/Tasks/TaskHelper.cs create mode 100644 backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs create mode 100644 backend/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs create mode 100644 backend/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs create mode 100644 backend/src/Squidex.Infrastructure/Translations/ITranslator.cs create mode 100644 backend/src/Squidex.Infrastructure/Translations/NoopTranslator.cs create mode 100644 backend/src/Squidex.Infrastructure/Translations/Translation.cs rename {src => backend/src}/Squidex.Infrastructure/Translations/TranslationResult.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs create mode 100644 backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs rename {src => backend/src}/Squidex.Infrastructure/UsageTracking/Counters.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/UsageTracking/DateUsage.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs create mode 100644 backend/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs rename {src => backend/src}/Squidex.Infrastructure/UsageTracking/Usage.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Validation/IValidatable.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/Validation/Not.cs (100%) create mode 100644 backend/src/Squidex.Infrastructure/Validation/Validate.cs create mode 100644 backend/src/Squidex.Infrastructure/Validation/ValidationError.cs create mode 100644 backend/src/Squidex.Infrastructure/Validation/ValidationException.cs rename {src => backend/src}/Squidex.Infrastructure/Validation/ValidationExtensions.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/ValueStopwatch.cs (100%) rename {src => backend/src}/Squidex.Infrastructure/language-codes.csv (100%) rename {src => backend/src}/Squidex.Shared/DefaultClients.cs (100%) rename {src => backend/src}/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs (100%) rename {src => backend/src}/Squidex.Shared/Identity/SquidexClaimTypes.cs (100%) create mode 100644 backend/src/Squidex.Shared/Permissions.cs create mode 100644 backend/src/Squidex.Shared/Squidex.Shared.csproj create mode 100644 backend/src/Squidex.Shared/Users/ClientUser.cs rename {src => backend/src}/Squidex.Shared/Users/IUser.cs (100%) create mode 100644 backend/src/Squidex.Shared/Users/IUserResolver.cs create mode 100644 backend/src/Squidex.Shared/Users/UserExtensions.cs create mode 100644 backend/src/Squidex.Web/ApiController.cs rename {src => backend/src}/Squidex.Web/ApiCostsAttribute.cs (100%) create mode 100644 backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs rename {src => backend/src}/Squidex.Web/ApiModelValidationAttribute.cs (100%) rename {src => backend/src}/Squidex.Web/ApiPermissionAttribute.cs (100%) create mode 100644 backend/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs rename {src => backend/src}/Squidex.Web/ClearCookiesAttribute.cs (100%) rename {src => backend/src}/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs (100%) rename {src => backend/src}/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs (100%) rename {src => backend/src}/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs (100%) create mode 100644 backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs rename {src => backend/src}/Squidex.Web/Constants.cs (100%) rename {src => backend/src}/Squidex.Web/ContextExtensions.cs (100%) create mode 100644 backend/src/Squidex.Web/ContextProvider.cs create mode 100644 backend/src/Squidex.Web/Deferred.cs rename {src => backend/src}/Squidex.Web/ETagExtensions.cs (100%) create mode 100644 backend/src/Squidex.Web/EntityCreatedDto.cs rename {src => backend/src}/Squidex.Web/ErrorDto.cs (100%) rename {src => backend/src}/Squidex.Web/ExposedConfiguration.cs (100%) create mode 100644 backend/src/Squidex.Web/ExposedValues.cs create mode 100644 backend/src/Squidex.Web/Extensions.cs create mode 100644 backend/src/Squidex.Web/FileCallbackResult.cs rename {src => backend/src}/Squidex.Web/FileExtensions.cs (100%) rename {src => backend/src}/Squidex.Web/IApiCostsFeature.cs (100%) create mode 100644 backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs create mode 100644 backend/src/Squidex.Web/PermissionExtensions.cs rename {src => backend/src}/Squidex.Web/Pipeline/ActionContextLogAppender.cs (100%) create mode 100644 backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs rename {src => backend/src}/Squidex.Web/Pipeline/ApiPermissionUnifier.cs (100%) create mode 100644 backend/src/Squidex.Web/Pipeline/AppResolver.cs rename {src => backend/src}/Squidex.Web/Pipeline/CleanupHostMiddleware.cs (100%) rename {src => backend/src}/Squidex.Web/Pipeline/DeferredActionFilter.cs (100%) rename {src => backend/src}/Squidex.Web/Pipeline/ETagFilter.cs (100%) rename {src => backend/src}/Squidex.Web/Pipeline/ETagOptions.cs (100%) rename {src => backend/src}/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs (100%) rename {src => backend/src}/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs (100%) create mode 100644 backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs rename {src => backend/src}/Squidex.Web/Pipeline/MeasureResultFilter.cs (100%) rename {src => backend/src}/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs (100%) create mode 100644 backend/src/Squidex.Web/Resource.cs create mode 100644 backend/src/Squidex.Web/ResourceLink.cs create mode 100644 backend/src/Squidex.Web/Services/UrlGenerator.cs create mode 100644 backend/src/Squidex.Web/Squidex.Web.csproj rename {src => backend/src}/Squidex.Web/SquidexWeb.cs (100%) create mode 100644 backend/src/Squidex.Web/UrlHelperExtensions.cs rename {src => backend/src}/Squidex.Web/UrlsOptions.cs (100%) rename {src => backend/src}/Squidex.Web/UsageOptions.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs rename {src => backend/src}/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Config/OpenApi/ODataQueryParamsProcessor.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiExtensions.cs create mode 100644 backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs create mode 100644 backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs rename {src => backend/src}/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs create mode 100644 backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsMetadata.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Assets/Models/AssetMetadata.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Assets/Models/AssetQuery.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Backups/Models/RestoreJobDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Docs/DocsController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/DocsVM.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/History/HistoryController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/LanguageDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/News/Models/FeaturesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/News/NewsController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Ping/PingController.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Rules/RulesController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/ChangeCategoryDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentCallsDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Statistics/Models/LogDownloadDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/UI/Models/UpdateSettingDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/UI/UIController.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Users/Assets/Avatar.png (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs (100%) rename {src => backend/src}/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs rename {src => backend/src}/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs (100%) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs create mode 100644 backend/src/Squidex/Areas/Api/Startup.cs rename {src => backend/src}/Squidex/Areas/Api/Views/Shared/Docs.cshtml (100%) rename {src => backend/src}/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs (100%) rename {src => backend/src}/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs (100%) create mode 100644 backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs create mode 100644 backend/src/Squidex/Areas/Frontend/Startup.cs create mode 100644 backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.crt create mode 100644 backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.key create mode 100644 backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.pfx create mode 100644 backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminHost.cs create mode 100644 backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs create mode 100644 backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs create mode 100644 backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs create mode 100644 backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs rename {src => backend/src}/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs (100%) create mode 100644 backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs rename {src => backend/src}/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs (100%) create mode 100644 backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs create mode 100644 backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs rename {src => backend/src}/Squidex/Areas/IdentityServer/Controllers/Error/ErrorViewModel.cs (100%) create mode 100644 backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs rename {src => backend/src}/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs (100%) rename {src => backend/src}/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs (100%) rename {src => backend/src}/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs (100%) rename {src => backend/src}/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs (100%) create mode 100644 backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs create mode 100644 backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs rename {src => backend/src}/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs (100%) rename {src => backend/src}/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs (100%) create mode 100644 backend/src/Squidex/Areas/IdentityServer/Startup.cs rename {src => backend/src}/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml (100%) rename {src => backend/src}/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml (100%) rename {src => backend/src}/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml (100%) rename {src => backend/src}/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml (100%) rename {src => backend/src}/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml (100%) rename {src => backend/src}/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml (100%) create mode 100644 backend/src/Squidex/Areas/IdentityServer/Views/Extensions.cs rename {src => backend/src}/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml (100%) rename {src => backend/src}/Squidex/Areas/IdentityServer/Views/_Layout.cshtml (100%) rename {src => backend/src}/Squidex/Areas/IdentityServer/Views/_ViewImports.cshtml (100%) rename {src => backend/src}/Squidex/Areas/IdentityServer/Views/_ViewStart.cshtml (100%) rename {src => backend/src}/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs (100%) create mode 100644 backend/src/Squidex/Areas/OrleansDashboard/Startup.cs rename {src => backend/src}/Squidex/Areas/Portal/Middlewares/PortalDashboardAuthenticationMiddleware.cs (100%) create mode 100644 backend/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs create mode 100644 backend/src/Squidex/Areas/Portal/Startup.cs create mode 100644 backend/src/Squidex/Config/Authentication/AuthenticationServices.cs create mode 100644 backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs rename {src => backend/src}/Squidex/Config/Authentication/GithubHandler.cs (100%) create mode 100644 backend/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs create mode 100644 backend/src/Squidex/Config/Authentication/GoogleHandler.cs create mode 100644 backend/src/Squidex/Config/Authentication/IdentityServerServices.cs create mode 100644 backend/src/Squidex/Config/Authentication/IdentityServices.cs create mode 100644 backend/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs create mode 100644 backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs rename {src => backend/src}/Squidex/Config/Authentication/OidcHandler.cs (100%) create mode 100644 backend/src/Squidex/Config/Authentication/OidcServices.cs create mode 100644 backend/src/Squidex/Config/Domain/AppsServices.cs create mode 100644 backend/src/Squidex/Config/Domain/AssetServices.cs create mode 100644 backend/src/Squidex/Config/Domain/BackupsServices.cs create mode 100644 backend/src/Squidex/Config/Domain/CommandsServices.cs create mode 100644 backend/src/Squidex/Config/Domain/CommentsServices.cs create mode 100644 backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs create mode 100644 backend/src/Squidex/Config/Domain/ContentsServices.cs create mode 100644 backend/src/Squidex/Config/Domain/EventPublishersServices.cs create mode 100644 backend/src/Squidex/Config/Domain/EventSourcingServices.cs create mode 100644 backend/src/Squidex/Config/Domain/HealthCheckServices.cs create mode 100644 backend/src/Squidex/Config/Domain/HistoryServices.cs create mode 100644 backend/src/Squidex/Config/Domain/InfrastructureServices.cs create mode 100644 backend/src/Squidex/Config/Domain/LoggingServices.cs create mode 100644 backend/src/Squidex/Config/Domain/MigrationServices.cs create mode 100644 backend/src/Squidex/Config/Domain/NotificationsServices.cs create mode 100644 backend/src/Squidex/Config/Domain/QueryServices.cs create mode 100644 backend/src/Squidex/Config/Domain/RuleServices.cs create mode 100644 backend/src/Squidex/Config/Domain/SchemasServices.cs rename {src => backend/src}/Squidex/Config/Domain/SerializationInitializer.cs (100%) create mode 100644 backend/src/Squidex/Config/Domain/SerializationServices.cs create mode 100644 backend/src/Squidex/Config/Domain/StoreServices.cs create mode 100644 backend/src/Squidex/Config/Domain/SubscriptionServices.cs rename {src => backend/src}/Squidex/Config/MyIdentityOptions.cs (100%) rename {src => backend/src}/Squidex/Config/Orleans/Helper.cs (100%) create mode 100644 backend/src/Squidex/Config/Orleans/OrleansServices.cs create mode 100644 backend/src/Squidex/Config/Startup/BackgroundHost.cs create mode 100644 backend/src/Squidex/Config/Startup/InitializerHost.cs create mode 100644 backend/src/Squidex/Config/Startup/LogConfigurationHost.cs create mode 100644 backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs create mode 100644 backend/src/Squidex/Config/Startup/MigratorHost.cs create mode 100644 backend/src/Squidex/Config/Startup/SafeHostedService.cs create mode 100644 backend/src/Squidex/Config/Web/WebExtensions.cs create mode 100644 backend/src/Squidex/Config/Web/WebServices.cs rename {src => backend/src}/Squidex/Docs/schemabody.md (100%) rename {src => backend/src}/Squidex/Docs/schemaquery.md (100%) rename {src => backend/src}/Squidex/Docs/security.md (100%) create mode 100644 backend/src/Squidex/Pipeline/OpenApi/NSwagHelper.cs rename {src => backend/src}/Squidex/Pipeline/Plugins/MvcParts.cs (100%) create mode 100644 backend/src/Squidex/Pipeline/Plugins/PluginExtensions.cs create mode 100644 backend/src/Squidex/Pipeline/Plugins/PluginLoaders.cs create mode 100644 backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs rename {src => backend/src}/Squidex/Pipeline/Robots/RobotsTxtOptions.cs (100%) create mode 100644 backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs rename {src => backend/src}/Squidex/Pipeline/Squid/icon-happy-sm.svg (100%) rename {src => backend/src}/Squidex/Pipeline/Squid/icon-happy.svg (100%) rename {src => backend/src}/Squidex/Pipeline/Squid/icon-sad-sm.svg (100%) rename {src => backend/src}/Squidex/Pipeline/Squid/icon-sad.svg (100%) create mode 100644 backend/src/Squidex/Program.cs create mode 100644 backend/src/Squidex/Squidex.csproj create mode 100644 backend/src/Squidex/Startup.cs rename {src => backend/src}/Squidex/appsettings.json (100%) rename {src => backend/src}/Squidex/wwwroot/client-callback-popup.html (100%) rename {src => backend/src}/Squidex/wwwroot/client-callback-silent.html (100%) rename {src => backend/src}/Squidex/wwwroot/favicon.ico (100%) rename {src => backend/src}/Squidex/wwwroot/images/add-app.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/add-blog.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/add-identity.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/add-profile.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/asset_doc.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/asset_docx.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/asset_generic.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/asset_pdf.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/asset_ppt.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/asset_pptx.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/asset_video.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/asset_xls.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/asset_xlsx.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/client.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/client.svg (100%) rename {src => backend/src}/Squidex/wwwroot/images/dashboard-api.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/dashboard-feedback.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/dashboard-github.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/dashboard-schema.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/loader-white.gif (100%) rename {src => backend/src}/Squidex/wwwroot/images/loader.gif (100%) rename {src => backend/src}/Squidex/wwwroot/images/login-icon.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/logo-half.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/logo-small.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/logo-squared-120.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/logo-white-small.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/logo-white.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/logo.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/onboarding-background.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/onboarding-step1.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/onboarding-step2.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/onboarding-step3.png (100%) rename {src => backend/src}/Squidex/wwwroot/images/onboarding-step4.png (100%) rename {src => backend/src}/Squidex/wwwroot/scripts/combined-editor.html (100%) rename {src => backend/src}/Squidex/wwwroot/scripts/context-editor.html (100%) rename {src => backend/src}/Squidex/wwwroot/scripts/editor-sdk.js (100%) create mode 100644 backend/src/Squidex/wwwroot/scripts/oidc-client.min.js rename {src => backend/src}/Squidex/wwwroot/scripts/simple-editor.html (100%) rename stylecop.json => backend/stylecop.json (100%) rename {tests => backend/tests}/RunCoverage.ps1 (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsJsonTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppImageTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternJsonTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPlanTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigJsonTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceFormattingTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleElementRegistryTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/AlwaysCreateClientCommandMiddlewareTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Assets/ImageTagGeneratorTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Contents/Text/GrainTextIndexerTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerGrainTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailSenderTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ArrayFieldPropertiesTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/UIFieldPropertiesTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/JsonHelper.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs rename {tests => backend/tests}/Squidex.Domain.Apps.Entities.Tests/TestHelpers/ValidationAssert.cs (100%) rename {tests => backend/tests}/Squidex.Domain.Users.Tests/AssetUserPictureStoreTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs rename {tests => backend/tests}/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs (100%) create mode 100644 backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/AssetExtensionTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreFixture.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreFixture.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreFixture.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreFixture.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/HasherStreamTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/Images/logo.png (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/MemoryAssetStoreTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/MongoGridFSAssetStoreFixture.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreFixture.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeHeadersTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/FileExtensionsTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/GravatarHelperTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/GuardTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/InstantExtensions.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Log/SemanticLogAdapterTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/MongoDb/BsonConverterTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Net/IPAddressComparerTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/LockGrainTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/RandomHashTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/RetryWindowTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Security/PermissionTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj rename {tests => backend/tests}/Squidex.Infrastructure.Tests/States/DefaultStreamNameResolverTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/StringExtensionsTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Tasks/AsyncLockPoolTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Tasks/AsyncLockTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs (100%) rename {tests => backend/tests}/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs (100%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs rename {tests => backend/tests}/Squidex.Infrastructure.Tests/ValidationExtensionsTests.cs (100%) rename {tests => backend/tests}/Squidex.Web.Tests/ApiCostsAttributeTests.cs (100%) create mode 100644 backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs create mode 100644 backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs create mode 100644 backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs create mode 100644 backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs create mode 100644 backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs create mode 100644 backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs rename {tests => backend/tests}/Squidex.Web.Tests/ExposedValuesTests.cs (100%) create mode 100644 backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs rename {tests => backend/tests}/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs (100%) create mode 100644 backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs rename {tests => backend/tests}/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs (100%) create mode 100644 backend/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs rename {tests => backend/tests}/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs (100%) create mode 100644 backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj rename {tests => backend/tests}/docker-compose.yml (100%) create mode 100644 backend/tools/GenerateLanguages/GenerateLanguages.csproj rename {tools => backend/tools}/GenerateLanguages/GenerateLanguages.sln (100%) rename {tools => backend/tools}/GenerateLanguages/Program.cs (100%) create mode 100644 backend/tools/LoadTest/LoadTest.csproj rename {tools => backend/tools}/LoadTest/LoadTest.sln (100%) rename {tools => backend/tools}/LoadTest/Model/TestClient.cs (100%) rename {tools => backend/tools}/LoadTest/Model/TestEntity.cs (100%) rename {tools => backend/tools}/LoadTest/ReadingBenchmarks.cs (100%) rename {tools => backend/tools}/LoadTest/ReadingFixture.cs (100%) rename {tools => backend/tools}/LoadTest/Run.cs (100%) rename {tools => backend/tools}/LoadTest/TestUtils.cs (100%) rename {tools => backend/tools}/LoadTest/Utils/Run.cs (100%) rename {tools => backend/tools}/LoadTest/WritingBenchmarks.cs (100%) rename {tools => backend/tools}/LoadTest/WritingFixture.cs (100%) create mode 100644 backend/tools/Migrate_00/Migrate_00.csproj rename {tools => backend/tools}/Migrate_00/Program.cs (100%) create mode 100644 backend/tools/Migrate_01/Migrate_01.csproj create mode 100644 backend/tools/Migrate_01/MigrationPath.cs create mode 100644 backend/tools/Migrate_01/Migrations/AddPatterns.cs rename {tools => backend/tools}/Migrate_01/Migrations/ClearSchemas.cs (100%) create mode 100644 backend/tools/Migrate_01/Migrations/ConvertEventStore.cs create mode 100644 backend/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs rename {tools => backend/tools}/Migrate_01/Migrations/CreateAssetSlugs.cs (100%) rename {tools => backend/tools}/Migrate_01/Migrations/MongoDb/ConvertOldSnapshotStores.cs (100%) rename {tools => backend/tools}/Migrate_01/Migrations/MongoDb/ConvertRuleEventsJson.cs (100%) rename {tools => backend/tools}/Migrate_01/Migrations/MongoDb/DeleteContentCollections.cs (100%) rename {tools => backend/tools}/Migrate_01/Migrations/MongoDb/RenameAssetSlugField.cs (100%) rename {tools => backend/tools}/Migrate_01/Migrations/MongoDb/RestructureContentCollection.cs (100%) rename {tools => backend/tools}/Migrate_01/Migrations/PopulateGrainIndexes.cs (100%) rename {tools => backend/tools}/Migrate_01/Migrations/RebuildApps.cs (100%) rename {tools => backend/tools}/Migrate_01/Migrations/RebuildAssets.cs (100%) rename {tools => backend/tools}/Migrate_01/Migrations/RebuildContents.cs (100%) rename {tools => backend/tools}/Migrate_01/Migrations/RebuildSnapshots.cs (100%) rename {tools => backend/tools}/Migrate_01/Migrations/StartEventConsumers.cs (100%) rename {tools => backend/tools}/Migrate_01/Migrations/StopEventConsumers.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/AppClientChanged.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/AppClientPermission.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/AppClientUpdated.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/AppContributorAssigned.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/AppContributorPermission.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/AppPlanChanged.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/AppWorkflowConfigured.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/AssetRenamed.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/AssetTagged.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/ContentArchived.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/ContentCreated.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/ContentPublished.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/ContentRestored.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/ContentStatusChanged.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/ContentUnpublished.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/SchemaCreated.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/ScriptsConfigured.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/WebhookAdded.cs (100%) rename {tools => backend/tools}/Migrate_01/OldEvents/WebhookDeleted.cs (100%) rename {tools => backend/tools}/Migrate_01/OldTriggers/AssetChangedTrigger.cs (100%) rename {tools => backend/tools}/Migrate_01/OldTriggers/ContentChangedTrigger.cs (100%) rename {tools => backend/tools}/Migrate_01/OldTriggers/ContentChangedTriggerSchema.cs (100%) rename {tools => backend/tools}/Migrate_01/RebuildOptions.cs (100%) create mode 100644 backend/tools/Migrate_01/RebuildRunner.cs rename {tools => backend/tools}/Migrate_01/Rebuilder.cs (100%) rename {tools => backend/tools}/Migrate_01/SquidexMigrations.cs (100%) delete mode 100644 extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs delete mode 100644 extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs delete mode 100644 extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs delete mode 100644 extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs delete mode 100644 extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs delete mode 100644 extensions/Squidex.Extensions/Squidex.Extensions.csproj rename {src/Squidex => frontend}/.sass-lint.yml (100%) rename {src/Squidex => frontend}/app-config/karma-test-shim.js (100%) rename {src/Squidex => frontend}/app-config/karma.conf.js (100%) rename {src/Squidex => frontend}/app-config/karma.coverage.conf.js (100%) create mode 100644 frontend/app-config/webpack.config.js rename {src/Squidex/wwwroot => frontend/app}/_theme.html (100%) rename {src/Squidex => frontend}/app/app.component.html (100%) rename {src/Squidex => frontend}/app/app.component.scss (100%) rename {src/Squidex => frontend}/app/app.component.ts (100%) rename {src/Squidex => frontend}/app/app.module.ts (100%) rename {src/Squidex => frontend}/app/app.routes.ts (100%) rename {src/Squidex => frontend}/app/app.ts (100%) rename {src/Squidex => frontend}/app/declarations.d.ts (100%) rename {src/Squidex => frontend}/app/features/administration/administration-area.component.html (100%) rename {src/Squidex => frontend}/app/features/administration/administration-area.component.scss (100%) rename {src/Squidex => frontend}/app/features/administration/administration-area.component.ts (100%) rename {src/Squidex => frontend}/app/features/administration/declarations.ts (100%) rename {src/Squidex => frontend}/app/features/administration/guards/unset-user.guard.spec.ts (100%) rename {src/Squidex => frontend}/app/features/administration/guards/unset-user.guard.ts (100%) rename {src/Squidex => frontend}/app/features/administration/guards/user-must-exist.guard.spec.ts (100%) rename {src/Squidex => frontend}/app/features/administration/guards/user-must-exist.guard.ts (100%) rename {src/Squidex => frontend}/app/features/administration/internal.ts (100%) rename {src/Squidex => frontend}/app/features/administration/module.ts (100%) rename {src/Squidex => frontend}/app/features/administration/pages/event-consumers/event-consumer.component.ts (100%) rename {src/Squidex => frontend}/app/features/administration/pages/event-consumers/event-consumers-page.component.html (100%) rename {src/Squidex => frontend}/app/features/administration/pages/event-consumers/event-consumers-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/administration/pages/event-consumers/event-consumers-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/administration/pages/restore/restore-page.component.html (100%) rename {src/Squidex => frontend}/app/features/administration/pages/restore/restore-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/administration/pages/restore/restore-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/administration/pages/users/user-page.component.html (100%) rename {src/Squidex => frontend}/app/features/administration/pages/users/user-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/administration/pages/users/user-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/administration/pages/users/user.component.ts (100%) rename {src/Squidex => frontend}/app/features/administration/pages/users/users-page.component.html (100%) rename {src/Squidex => frontend}/app/features/administration/pages/users/users-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/administration/pages/users/users-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/administration/services/event-consumers.service.spec.ts (100%) rename {src/Squidex => frontend}/app/features/administration/services/event-consumers.service.ts (100%) rename {src/Squidex => frontend}/app/features/administration/services/users.service.spec.ts (100%) rename {src/Squidex => frontend}/app/features/administration/services/users.service.ts (100%) rename {src/Squidex => frontend}/app/features/administration/state/event-consumers.state.spec.ts (100%) rename {src/Squidex => frontend}/app/features/administration/state/event-consumers.state.ts (100%) rename {src/Squidex => frontend}/app/features/administration/state/users.forms.ts (100%) rename {src/Squidex => frontend}/app/features/administration/state/users.state.spec.ts (100%) rename {src/Squidex => frontend}/app/features/administration/state/users.state.ts (100%) rename {src/Squidex => frontend}/app/features/api/api-area.component.html (100%) rename {src/Squidex => frontend}/app/features/api/api-area.component.scss (100%) rename {src/Squidex => frontend}/app/features/api/api-area.component.ts (100%) rename {src/Squidex => frontend}/app/features/api/declarations.ts (100%) rename {src/Squidex => frontend}/app/features/api/index.ts (100%) rename {src/Squidex => frontend}/app/features/api/module.ts (100%) rename {src/Squidex => frontend}/app/features/api/pages/graphql/graphql-page.component.html (100%) rename {src/Squidex => frontend}/app/features/api/pages/graphql/graphql-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/api/pages/graphql/graphql-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/apps/declarations.ts (100%) rename {src/Squidex => frontend}/app/features/apps/index.ts (100%) rename {src/Squidex => frontend}/app/features/apps/module.ts (100%) rename {src/Squidex => frontend}/app/features/apps/pages/apps-page.component.html (100%) rename {src/Squidex => frontend}/app/features/apps/pages/apps-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/apps/pages/apps-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/apps/pages/news-dialog.component.html (100%) rename {src/Squidex => frontend}/app/features/apps/pages/news-dialog.component.scss (100%) rename {src/Squidex => frontend}/app/features/apps/pages/news-dialog.component.ts (100%) rename {src/Squidex => frontend}/app/features/apps/pages/onboarding-dialog.component.html (100%) rename {src/Squidex => frontend}/app/features/apps/pages/onboarding-dialog.component.scss (100%) rename {src/Squidex => frontend}/app/features/apps/pages/onboarding-dialog.component.ts (100%) rename {src/Squidex => frontend}/app/features/assets/declarations.ts (100%) rename {src/Squidex => frontend}/app/features/assets/index.ts (100%) rename {src/Squidex => frontend}/app/features/assets/module.ts (100%) rename {src/Squidex => frontend}/app/features/assets/pages/assets-filters-page.component.html (100%) rename {src/Squidex => frontend}/app/features/assets/pages/assets-filters-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/assets/pages/assets-filters-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/assets/pages/assets-page.component.html (100%) rename {src/Squidex => frontend}/app/features/assets/pages/assets-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/assets/pages/assets-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/declarations.ts (100%) rename {src/Squidex => frontend}/app/features/content/index.ts (100%) rename {src/Squidex => frontend}/app/features/content/module.ts (100%) rename {src/Squidex => frontend}/app/features/content/pages/comments/comments-page.component.html (100%) rename {src/Squidex => frontend}/app/features/content/pages/comments/comments-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/pages/comments/comments-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/pages/content/content-field.component.html (100%) rename {src/Squidex => frontend}/app/features/content/pages/content/content-field.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/pages/content/content-field.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/pages/content/content-history-page.component.html (100%) rename {src/Squidex => frontend}/app/features/content/pages/content/content-history-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/pages/content/content-history-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/pages/content/content-page.component.html (100%) rename {src/Squidex => frontend}/app/features/content/pages/content/content-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/pages/content/content-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/pages/content/field-languages.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/pages/contents/contents-filters-page.component.html (100%) rename {src/Squidex => frontend}/app/features/content/pages/contents/contents-filters-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/pages/contents/contents-filters-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/pages/contents/contents-page.component.html (100%) rename {src/Squidex => frontend}/app/features/content/pages/contents/contents-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/pages/contents/contents-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/pages/messages.ts (100%) rename {src/Squidex => frontend}/app/features/content/pages/schemas/schemas-page.component.html (100%) rename {src/Squidex => frontend}/app/features/content/pages/schemas/schemas-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/pages/schemas/schemas-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/shared/array-editor.component.html (100%) rename {src/Squidex => frontend}/app/features/content/shared/array-editor.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/shared/array-editor.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/shared/array-item.component.html (100%) rename {src/Squidex => frontend}/app/features/content/shared/array-item.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/shared/array-item.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/shared/assets-editor.component.html (100%) rename {src/Squidex => frontend}/app/features/content/shared/assets-editor.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/shared/assets-editor.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/shared/content-selector-item.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/shared/content-status.component.html (100%) rename {src/Squidex => frontend}/app/features/content/shared/content-status.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/shared/content-status.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/shared/content-value-editor.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/shared/content-value.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/shared/content.component.html (100%) rename {src/Squidex => frontend}/app/features/content/shared/content.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/shared/content.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/shared/contents-selector.component.html (100%) rename {src/Squidex => frontend}/app/features/content/shared/contents-selector.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/shared/contents-selector.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/shared/due-time-selector.component.html (100%) rename {src/Squidex => frontend}/app/features/content/shared/due-time-selector.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/shared/due-time-selector.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/shared/field-editor.component.html (100%) rename {src/Squidex => frontend}/app/features/content/shared/field-editor.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/shared/field-editor.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/shared/preview-button.component.html (100%) rename {src/Squidex => frontend}/app/features/content/shared/preview-button.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/shared/preview-button.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/shared/reference-item.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/shared/reference-item.component.ts (100%) rename {src/Squidex => frontend}/app/features/content/shared/references-editor.component.html (100%) rename {src/Squidex => frontend}/app/features/content/shared/references-editor.component.scss (100%) rename {src/Squidex => frontend}/app/features/content/shared/references-editor.component.ts (100%) rename {src/Squidex => frontend}/app/features/dashboard/declarations.ts (100%) rename {src/Squidex => frontend}/app/features/dashboard/index.ts (100%) rename {src/Squidex => frontend}/app/features/dashboard/module.ts (100%) rename {src/Squidex => frontend}/app/features/dashboard/pages/dashboard-page.component.html (100%) rename {src/Squidex => frontend}/app/features/dashboard/pages/dashboard-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/dashboard/pages/dashboard-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/rules/declarations.ts (100%) rename {src/Squidex => frontend}/app/features/rules/index.ts (100%) rename {src/Squidex => frontend}/app/features/rules/module.ts (100%) rename {src/Squidex => frontend}/app/features/rules/pages/events/pipes.ts (100%) rename {src/Squidex => frontend}/app/features/rules/pages/events/rule-events-page.component.html (100%) rename {src/Squidex => frontend}/app/features/rules/pages/events/rule-events-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/rules/pages/events/rule-events-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/actions/generic-action.component.html (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/actions/generic-action.component.scss (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/actions/generic-action.component.ts (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/rule-element.component.html (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/rule-element.component.scss (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/rule-element.component.ts (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/rule-icon.component.ts (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/rule-wizard.component.html (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/rule-wizard.component.scss (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/rule-wizard.component.ts (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/rule.component.html (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/rule.component.scss (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/rule.component.ts (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/rules-page.component.html (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/rules-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/rules-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.scss (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.html (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.scss (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/triggers/usage-trigger.component.html (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/triggers/usage-trigger.component.scss (100%) rename {src/Squidex => frontend}/app/features/rules/pages/rules/triggers/usage-trigger.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/declarations.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/index.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/module.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/messages.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/field-wizard.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/field-wizard.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/field-wizard.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/field.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/field.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/field.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/forms/field-form-common.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/forms/field-form-ui.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/forms/field-form-validation.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/forms/field-form.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-edit-form.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-edit-form.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-edit-form.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-export-form.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-export-form.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-export-form.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-page.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-preview-urls-form.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-preview-urls-form.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-scripts-form.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-scripts-form.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/schema-scripts-form.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/array-validation.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/array-validation.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/array-validation.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/assets-ui.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/assets-ui.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/assets-ui.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/assets-validation.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/assets-validation.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/assets-validation.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/boolean-ui.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/boolean-ui.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/boolean-ui.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/boolean-validation.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/boolean-validation.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/boolean-validation.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/date-time-ui.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/date-time-ui.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/date-time-ui.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/date-time-validation.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/date-time-validation.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/date-time-validation.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/geolocation-ui.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/geolocation-ui.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/geolocation-ui.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/geolocation-validation.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/geolocation-validation.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/geolocation-validation.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/json-ui.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/json-ui.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/json-ui.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/json-validation.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/json-validation.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/json-validation.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/number-ui.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/number-ui.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/number-ui.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/number-validation.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/number-validation.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/number-validation.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/references-ui.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/references-ui.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/references-ui.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/references-validation.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/references-validation.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/references-validation.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/string-ui.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/string-ui.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/string-ui.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/string-validation.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/string-validation.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/string-validation.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/tags-ui.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/tags-ui.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/tags-ui.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/tags-validation.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/tags-validation.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schema/types/tags-validation.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schemas/schema-form.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schemas/schema-form.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schemas/schema-form.component.ts (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schemas/schemas-page.component.html (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schemas/schemas-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/schemas/pages/schemas/schemas-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/declarations.ts (100%) rename {src/Squidex => frontend}/app/features/settings/index.ts (100%) rename {src/Squidex => frontend}/app/features/settings/module.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/backups/backup.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/backups/backups-page.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/backups/backups-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/backups/backups-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/clients/client-add-form.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/clients/client.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/clients/client.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/clients/client.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/clients/clients-page.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/clients/clients-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/clients/clients-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/contributors/contributor-add-form.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/contributors/contributor-add-form.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/contributors/contributor-add-form.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/contributors/contributor.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/contributors/contributors-page.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/contributors/contributors-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/contributors/contributors-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/contributors/import-contributors-dialog.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/contributors/import-contributors-dialog.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/contributors/import-contributors-dialog.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/languages/language-add-form.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/languages/language.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/languages/language.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/languages/language.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/languages/languages-page.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/languages/languages-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/languages/languages-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/more/more-page.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/more/more-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/more/more-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/patterns/pattern.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/patterns/pattern.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/patterns/pattern.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/patterns/patterns-page.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/patterns/patterns-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/patterns/patterns-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/plans/plan.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/plans/plan.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/plans/plan.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/plans/plans-page.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/plans/plans-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/plans/plans-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/roles/role-add-form.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/roles/role.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/roles/role.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/roles/role.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/roles/roles-page.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/roles/roles-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/roles/roles-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/workflows/workflow-add-form.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/workflows/workflow-step.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/workflows/workflow-step.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/workflows/workflow-step.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/workflows/workflow-transition.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/workflows/workflow-transition.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/workflows/workflow-transition.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/workflows/workflow.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/workflows/workflow.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/workflows/workflow.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/pages/workflows/workflows-page.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/pages/workflows/workflows-page.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/pages/workflows/workflows-page.component.ts (100%) rename {src/Squidex => frontend}/app/features/settings/settings-area.component.html (100%) rename {src/Squidex => frontend}/app/features/settings/settings-area.component.scss (100%) rename {src/Squidex => frontend}/app/features/settings/settings-area.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/animations.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/avatar.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/code.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/code.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/code.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/drag-helper.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/external-link.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/autocomplete.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/autocomplete.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/autocomplete.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/checkbox-group.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/checkbox-group.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/checkbox-group.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/code-editor.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/code-editor.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/code-editor.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/color-picker.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/color-picker.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/color-picker.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/confirm-click.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/control-errors.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/control-errors.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/control-errors.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/copy.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/date-time-editor.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/date-time-editor.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/date-time-editor.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/dropdown.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/dropdown.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/dropdown.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/editable-title.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/editable-title.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/editable-title.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/error-formatting.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/error-formatting.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/file-drop.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/focus-on-init.directive.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/focus-on-init.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/form-alert.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/form-error.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/form-hint.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/forms-helper.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/iframe-editor.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/iframe-editor.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/iframe-editor.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/indeterminate-value.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/json-editor.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/json-editor.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/json-editor.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/progress-bar.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/stars.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/stars.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/stars.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/tag-editor.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/tag-editor.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/tag-editor.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/toggle.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/toggle.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/toggle.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/transform-input.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/validators.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/forms/validators.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/highlight.pipe.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/hover-background.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/http/caching.interceptor.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/http/http-extensions.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/http/loading.interceptor.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/ignore-scrollbar.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/image-source.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/modals/dialog-renderer.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/modals/dialog-renderer.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/modals/dialog-renderer.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/modals/modal-dialog.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/modals/modal-dialog.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/modals/modal-dialog.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/modals/modal-placement.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/modals/modal.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/modals/onboarding-tooltip.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/modals/onboarding-tooltip.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/modals/onboarding-tooltip.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/modals/root-view.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/modals/tooltip.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pager.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/pager.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/pager.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/panel-container.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/panel.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/panel.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/panel.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pipes/colors.pipes.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pipes/colors.pipes.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pipes/date-time.pipes.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pipes/date-time.pipes.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pipes/keys.pipe.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pipes/keys.pipe.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pipes/markdown.pipe.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pipes/markdown.pipe.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pipes/money.pipe.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pipes/money.pipe.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pipes/name.pipe.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pipes/name.pipe.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pipes/numbers.pipes.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/pipes/numbers.pipes.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/popup-link.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/routers/can-deactivate.guard.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/routers/can-deactivate.guard.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/routers/parent-link.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/routers/router-utils.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/routers/router-utils.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/safe-html.pipe.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/scroll-active.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/shortcut.component.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/shortcut.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/stateful.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/status-icon.component.html (100%) rename {src/Squidex => frontend}/app/framework/angular/status-icon.component.scss (100%) rename {src/Squidex => frontend}/app/framework/angular/status-icon.component.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/stop-click.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/sync-scrolling.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/template-wrapper.directive.ts (100%) rename {src/Squidex => frontend}/app/framework/angular/title.component.ts (100%) rename {src/Squidex => frontend}/app/framework/configurations.ts (100%) rename {src/Squidex => frontend}/app/framework/declarations.ts (100%) rename {src/Squidex => frontend}/app/framework/index.ts (100%) rename {src/Squidex => frontend}/app/framework/internal.ts (100%) rename {src/Squidex => frontend}/app/framework/module.ts (100%) rename {src/Squidex => frontend}/app/framework/services/analytics.service.ts (100%) rename {src/Squidex => frontend}/app/framework/services/clipboard.service.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/services/clipboard.service.ts (100%) rename {src/Squidex => frontend}/app/framework/services/dialog.service.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/services/dialog.service.ts (100%) rename {src/Squidex => frontend}/app/framework/services/loading.service.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/services/loading.service.ts (100%) rename {src/Squidex => frontend}/app/framework/services/local-store.service.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/services/local-store.service.ts (100%) rename {src/Squidex => frontend}/app/framework/services/message-bus.service.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/services/message-bus.service.ts (100%) rename {src/Squidex => frontend}/app/framework/services/onboarding.service.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/services/onboarding.service.ts (100%) rename {src/Squidex => frontend}/app/framework/services/resource-loader.service.ts (100%) rename {src/Squidex => frontend}/app/framework/services/shortcut.service.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/services/shortcut.service.ts (100%) rename {src/Squidex => frontend}/app/framework/services/title.service.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/services/title.service.ts (100%) rename {src/Squidex => frontend}/app/framework/state.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/array-extensions.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/array-extensions.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/array-helper.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/date-helper.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/date-helper.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/date-time.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/date-time.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/duration.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/duration.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/error.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/error.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/hateos.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/interpolator.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/interpolator.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/keys.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/math-helper.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/math-helper.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/modal-positioner.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/modal-positioner.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/modal-view.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/modal-view.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/pager.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/pager.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/picasso.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/rxjs-extensions.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/string-helper.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/string-helper.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/types.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/types.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/version.spec.ts (100%) rename {src/Squidex => frontend}/app/framework/utils/version.ts (100%) rename {src/Squidex/wwwroot => frontend/app}/index.html (100%) rename {src/Squidex => frontend}/app/shared/components/app-form.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/app-form.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/app-form.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/asset-dialog.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/asset-dialog.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/asset-dialog.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/asset-uploader.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/asset-uploader.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/asset-uploader.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/asset.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/asset.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/asset.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/assets-list.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/assets-list.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/assets-list.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/assets-selector.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/assets-selector.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/assets-selector.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/comment.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/comment.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/comment.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/comments.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/comments.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/comments.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/geolocation-editor.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/geolocation-editor.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/geolocation-editor.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/help-markdown.pipe.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/components/help-markdown.pipe.ts (100%) rename {src/Squidex => frontend}/app/shared/components/help.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/help.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/help.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/history-list.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/history-list.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/history-list.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/history.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/history.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/history.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/language-selector.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/language-selector.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/language-selector.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/markdown-editor.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/markdown-editor.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/markdown-editor.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/pipes.ts (100%) rename {src/Squidex => frontend}/app/shared/components/queries/filter-comparison.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/queries/filter-comparison.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/queries/filter-comparison.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/queries/filter-logical.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/queries/filter-logical.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/queries/filter-logical.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/queries/filter-node.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/queries/query.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/queries/sorting.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/references-dropdown.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/rich-editor.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/rich-editor.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/rich-editor.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/saved-queries.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/schema-category.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/schema-category.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/schema-category.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/search-form.component.html (100%) rename {src/Squidex => frontend}/app/shared/components/search-form.component.scss (100%) rename {src/Squidex => frontend}/app/shared/components/search-form.component.ts (100%) rename {src/Squidex => frontend}/app/shared/components/table-header.component.ts (100%) rename {src/Squidex => frontend}/app/shared/declarations.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/app-must-exist.guard.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/app-must-exist.guard.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/content-must-exist.guard.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/content-must-exist.guard.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/load-apps.guard.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/load-apps.guard.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/load-languages.guard.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/load-languages.guard.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/must-be-authenticated.guard.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/must-be-authenticated.guard.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/must-be-not-authenticated.guard.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/must-be-not-authenticated.guard.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/schema-must-exist-published.guard.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/schema-must-exist-published.guard.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/schema-must-exist.guard.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/schema-must-exist.guard.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/schema-must-not-be-singleton.guard.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/unset-app.guard.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/unset-app.guard.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/unset-content.guard.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/guards/unset-content.guard.ts (100%) rename {src/Squidex => frontend}/app/shared/index.ts (100%) rename {src/Squidex => frontend}/app/shared/interceptors/auth.interceptor.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/interceptors/auth.interceptor.ts (100%) rename {src/Squidex => frontend}/app/shared/internal.ts (100%) rename {src/Squidex => frontend}/app/shared/module.ts (100%) rename {src/Squidex => frontend}/app/shared/services/app-languages.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/app-languages.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/apps.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/apps.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/assets.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/assets.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/auth.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/autosave.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/autosave.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/backups.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/backups.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/clients.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/clients.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/comments.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/comments.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/contents.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/contents.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/contributors.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/contributors.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/graphql.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/graphql.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/help.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/help.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/history.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/history.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/languages.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/languages.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/news.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/news.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/patterns.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/patterns.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/plans.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/plans.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/roles.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/roles.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/rules.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/rules.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/schemas.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/schemas.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/schemas.types.ts (100%) rename {src/Squidex => frontend}/app/shared/services/translations.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/translations.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/ui.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/ui.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/usages.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/usages.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/users-provider.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/users-provider.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/users.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/users.service.ts (100%) rename {src/Squidex => frontend}/app/shared/services/workflows.service.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/services/workflows.service.ts (100%) rename {src/Squidex => frontend}/app/shared/state/_test-helpers.ts (100%) rename {src/Squidex => frontend}/app/shared/state/apps.forms.ts (100%) rename {src/Squidex => frontend}/app/shared/state/apps.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/apps.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/asset-uploader.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/asset-uploader.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/assets.forms.ts (100%) rename {src/Squidex => frontend}/app/shared/state/assets.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/assets.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/backups.forms.ts (100%) rename {src/Squidex => frontend}/app/shared/state/backups.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/backups.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/clients.forms.ts (100%) rename {src/Squidex => frontend}/app/shared/state/clients.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/clients.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/comments.form.ts (100%) rename {src/Squidex => frontend}/app/shared/state/comments.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/comments.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/contents.forms.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/contents.forms.ts (100%) rename {src/Squidex => frontend}/app/shared/state/contents.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/contributors.forms.ts (100%) rename {src/Squidex => frontend}/app/shared/state/contributors.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/contributors.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/languages.forms.ts (100%) rename {src/Squidex => frontend}/app/shared/state/languages.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/languages.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/patterns.forms.ts (100%) rename {src/Squidex => frontend}/app/shared/state/patterns.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/patterns.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/plans.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/plans.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/queries.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/queries.ts (100%) rename {src/Squidex => frontend}/app/shared/state/query.ts (100%) rename {src/Squidex => frontend}/app/shared/state/roles.forms.ts (100%) rename {src/Squidex => frontend}/app/shared/state/roles.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/roles.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/rule-events.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/rule-events.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/rules.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/rules.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/schema-tag-converter.ts (100%) rename {src/Squidex => frontend}/app/shared/state/schemas.forms.ts (100%) rename {src/Squidex => frontend}/app/shared/state/schemas.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/schemas.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/ui.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/ui.state.ts (100%) rename {src/Squidex => frontend}/app/shared/state/workflows.forms.ts (100%) rename {src/Squidex => frontend}/app/shared/state/workflows.state.spec.ts (100%) rename {src/Squidex => frontend}/app/shared/state/workflows.state.ts (100%) rename {src/Squidex => frontend}/app/shared/utils/messages.ts (100%) rename {src/Squidex => frontend}/app/shell/declarations.ts (100%) rename {src/Squidex => frontend}/app/shell/index.ts (100%) rename {src/Squidex => frontend}/app/shell/module.ts (100%) rename {src/Squidex => frontend}/app/shell/pages/app/app-area.component.html (100%) rename {src/Squidex => frontend}/app/shell/pages/app/app-area.component.scss (100%) rename {src/Squidex => frontend}/app/shell/pages/app/app-area.component.ts (100%) rename {src/Squidex => frontend}/app/shell/pages/app/left-menu.component.html (100%) rename {src/Squidex => frontend}/app/shell/pages/app/left-menu.component.scss (100%) rename {src/Squidex => frontend}/app/shell/pages/app/left-menu.component.ts (100%) rename {src/Squidex => frontend}/app/shell/pages/forbidden/forbidden-page.component.ts (100%) rename {src/Squidex => frontend}/app/shell/pages/home/home-page.component.html (100%) rename {src/Squidex => frontend}/app/shell/pages/home/home-page.component.scss (100%) rename {src/Squidex => frontend}/app/shell/pages/home/home-page.component.ts (100%) rename {src/Squidex => frontend}/app/shell/pages/internal/apps-menu.component.html (100%) rename {src/Squidex => frontend}/app/shell/pages/internal/apps-menu.component.scss (100%) rename {src/Squidex => frontend}/app/shell/pages/internal/apps-menu.component.ts (100%) rename {src/Squidex => frontend}/app/shell/pages/internal/internal-area.component.html (100%) rename {src/Squidex => frontend}/app/shell/pages/internal/internal-area.component.scss (100%) rename {src/Squidex => frontend}/app/shell/pages/internal/internal-area.component.ts (100%) rename {src/Squidex => frontend}/app/shell/pages/internal/profile-menu.component.html (100%) rename {src/Squidex => frontend}/app/shell/pages/internal/profile-menu.component.scss (100%) rename {src/Squidex => frontend}/app/shell/pages/internal/profile-menu.component.ts (100%) rename {src/Squidex => frontend}/app/shell/pages/login/login-page.component.ts (100%) rename {src/Squidex => frontend}/app/shell/pages/logout/logout-page.component.ts (100%) rename {src/Squidex => frontend}/app/shell/pages/not-found/not-found-page.component.ts (100%) rename {src/Squidex => frontend}/app/shims.ts (100%) rename {src/Squidex => frontend}/app/theme/_bootstrap-vars.scss (100%) rename {src/Squidex => frontend}/app/theme/_bootstrap.scss (100%) rename {src/Squidex => frontend}/app/theme/_common.scss (100%) rename {src/Squidex => frontend}/app/theme/_forms.scss (100%) rename {src/Squidex => frontend}/app/theme/_lists.scss (100%) rename {src/Squidex => frontend}/app/theme/_mixins.scss (100%) rename {src/Squidex => frontend}/app/theme/_panels.scss (100%) rename {src/Squidex => frontend}/app/theme/_static.scss (100%) rename {src/Squidex => frontend}/app/theme/_vars.scss (100%) rename {src/Squidex => frontend}/app/theme/icomoon/demo-files/demo.css (100%) rename {src/Squidex => frontend}/app/theme/icomoon/demo-files/demo.js (100%) rename {src/Squidex => frontend}/app/theme/icomoon/demo.html (100%) rename {src/Squidex => frontend}/app/theme/icomoon/fonts/icomoon.eot (100%) rename {src/Squidex => frontend}/app/theme/icomoon/fonts/icomoon.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/fonts/icomoon.ttf (100%) rename {src/Squidex => frontend}/app/theme/icomoon/fonts/icomoon.woff (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/action-Algolia.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/action-Fastly.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/activity.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/add-app.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/add.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/api.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/assets.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/caret-bottom.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/caret-top.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/check-circle-filled.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/check-circle.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/client.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/close.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/contents.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/control-Checkbox.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/control-Checkboxes.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/control-Date.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/control-DateTime.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/control-Dropdown.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/control-Html.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/control-Input.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/control-Markdown.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/control-Radio.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/control-RichText.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/control-Slug.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/control-Tags.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/control-TextArea.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/control-Toggle.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/copy.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/dashboard-api.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/dashboard-feedback.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/dashboard-github.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/dashboard-schema.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/dashboard.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/delete-filled.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/delete.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/document-delete.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/document-disable.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/document-lock.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/document-publish.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/document-unpublish.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/drag.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/fastly.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/filter.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/help.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/hide-all.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/hide.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/json.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/location.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/logo.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/media.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/more.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/multiple-content.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/orleans.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/pencil.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/reference.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/schemas.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/search.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/settings.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/show-all.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/show.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/single-content.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/type-Array.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/type-Boolean.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/type-DateTime.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/type-Number.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/type-String.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/type-Tags.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/user-o.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/user.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/icons/webhooks.svg (100%) rename {src/Squidex => frontend}/app/theme/icomoon/selection.json (100%) rename {src/Squidex => frontend}/app/theme/icomoon/style.css (100%) rename {src/Squidex => frontend}/app/theme/theme.scss (100%) rename {src/Squidex => frontend}/karma.conf.js (100%) rename {src/Squidex => frontend}/karma.coverage.conf.js (100%) create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json rename {src/Squidex => frontend}/tsconfig.json (100%) rename {src/Squidex => frontend}/tslint.json (100%) delete mode 100644 libs/Dockerfile create mode 100644 libs/docker-compose.yml delete mode 100644 nuget.exe delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Named.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Partitioning.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj delete mode 100644 src/Squidex.Domain.Apps.Entities/AppProvider.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValue.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Text/IndexState.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexContent.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Context.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/EntityMapper.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/History/HistoryService.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailEventConsumer.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailSender.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/IAppProvider.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Q.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj delete mode 100644 src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs delete mode 100644 src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs delete mode 100644 src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs delete mode 100644 src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs delete mode 100644 src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs delete mode 100644 src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs delete mode 100644 src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj delete mode 100644 src/Squidex.Domain.Users.MongoDb/MongoUser.cs delete mode 100644 src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs delete mode 100644 src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj delete mode 100644 src/Squidex.Domain.Users/AssetUserPictureStore.cs delete mode 100644 src/Squidex.Domain.Users/DefaultUserResolver.cs delete mode 100644 src/Squidex.Domain.Users/DefaultXmlRepository.cs delete mode 100644 src/Squidex.Domain.Users/PwnedPasswordValidator.cs delete mode 100644 src/Squidex.Domain.Users/Squidex.Domain.Users.csproj delete mode 100644 src/Squidex.Domain.Users/UserManagerExtensions.cs delete mode 100644 src/Squidex.Domain.Users/UserWithClaims.cs delete mode 100644 src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs delete mode 100644 src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs delete mode 100644 src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs delete mode 100644 src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs delete mode 100644 src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs delete mode 100644 src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs delete mode 100644 src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs delete mode 100644 src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs delete mode 100644 src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj delete mode 100644 src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs delete mode 100644 src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs delete mode 100644 src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs delete mode 100644 src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs delete mode 100644 src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs delete mode 100644 src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj delete mode 100644 src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs delete mode 100644 src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj delete mode 100644 src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj delete mode 100644 src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs delete mode 100644 src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs delete mode 100644 src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs delete mode 100644 src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj delete mode 100644 src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj delete mode 100644 src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs delete mode 100644 src/Squidex.Infrastructure/Assets/AssetFile.cs delete mode 100644 src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs delete mode 100644 src/Squidex.Infrastructure/Assets/AssetStoreExtensions.cs delete mode 100644 src/Squidex.Infrastructure/Assets/FTPAssetStore.cs delete mode 100644 src/Squidex.Infrastructure/Assets/FolderAssetStore.cs delete mode 100644 src/Squidex.Infrastructure/Assets/HasherStream.cs delete mode 100644 src/Squidex.Infrastructure/Assets/IAssetStore.cs delete mode 100644 src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs delete mode 100644 src/Squidex.Infrastructure/Assets/ImageInfo.cs delete mode 100644 src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs delete mode 100644 src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs delete mode 100644 src/Squidex.Infrastructure/Assets/NoopAssetStore.cs delete mode 100644 src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs delete mode 100644 src/Squidex.Infrastructure/Caching/CachingProviderBase.cs delete mode 100644 src/Squidex.Infrastructure/Caching/ILocalCache.cs delete mode 100644 src/Squidex.Infrastructure/Caching/LRUCache.cs delete mode 100644 src/Squidex.Infrastructure/Caching/LRUCacheItem.cs delete mode 100644 src/Squidex.Infrastructure/CollectionExtensions.cs delete mode 100644 src/Squidex.Infrastructure/Collections/ArrayDictionary.cs delete mode 100644 src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs delete mode 100644 src/Squidex.Infrastructure/Commands/CommandContext.cs delete mode 100644 src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs delete mode 100644 src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs delete mode 100644 src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs delete mode 100644 src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs delete mode 100644 src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs delete mode 100644 src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs delete mode 100644 src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs delete mode 100644 src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs delete mode 100644 src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs delete mode 100644 src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs delete mode 100644 src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs delete mode 100644 src/Squidex.Infrastructure/DelegateDisposable.cs delete mode 100644 src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs delete mode 100644 src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs delete mode 100644 src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs delete mode 100644 src/Squidex.Infrastructure/DomainException.cs delete mode 100644 src/Squidex.Infrastructure/DomainObjectException.cs delete mode 100644 src/Squidex.Infrastructure/Email/SmtpEmailSender.cs delete mode 100644 src/Squidex.Infrastructure/EventSourcing/CompoundEventConsumer.cs delete mode 100644 src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs delete mode 100644 src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs delete mode 100644 src/Squidex.Infrastructure/EventSourcing/EventData.cs delete mode 100644 src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs delete mode 100644 src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs delete mode 100644 src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs delete mode 100644 src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs delete mode 100644 src/Squidex.Infrastructure/EventSourcing/IEventDataFormatter.cs delete mode 100644 src/Squidex.Infrastructure/EventSourcing/IEventStore.cs delete mode 100644 src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs delete mode 100644 src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs delete mode 100644 src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs delete mode 100644 src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs delete mode 100644 src/Squidex.Infrastructure/Guard.cs delete mode 100644 src/Squidex.Infrastructure/Http/DumpFormatter.cs delete mode 100644 src/Squidex.Infrastructure/Json/IJsonSerializer.cs delete mode 100644 src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs delete mode 100644 src/Squidex.Infrastructure/Json/Newtonsoft/InstantConverter.cs delete mode 100644 src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs delete mode 100644 src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs delete mode 100644 src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs delete mode 100644 src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs delete mode 100644 src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs delete mode 100644 src/Squidex.Infrastructure/Json/Objects/JsonArray.cs delete mode 100644 src/Squidex.Infrastructure/Json/Objects/JsonNull.cs delete mode 100644 src/Squidex.Infrastructure/Json/Objects/JsonObject.cs delete mode 100644 src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs delete mode 100644 src/Squidex.Infrastructure/Json/Objects/JsonValue.cs delete mode 100644 src/Squidex.Infrastructure/Language.cs delete mode 100644 src/Squidex.Infrastructure/LanguagesInitializer.cs delete mode 100644 src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs delete mode 100644 src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs delete mode 100644 src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs delete mode 100644 src/Squidex.Infrastructure/Log/FileChannel.cs delete mode 100644 src/Squidex.Infrastructure/Log/IObjectWriter.cs delete mode 100644 src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs delete mode 100644 src/Squidex.Infrastructure/Log/JsonLogWriter.cs delete mode 100644 src/Squidex.Infrastructure/Log/LockingLogStore.cs delete mode 100644 src/Squidex.Infrastructure/Log/Profiler.cs delete mode 100644 src/Squidex.Infrastructure/Log/ProfilerSession.cs delete mode 100644 src/Squidex.Infrastructure/Log/ProfilerSpan.cs delete mode 100644 src/Squidex.Infrastructure/Log/SemanticLog.cs delete mode 100644 src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs delete mode 100644 src/Squidex.Infrastructure/Log/TimestampLogAppender.cs delete mode 100644 src/Squidex.Infrastructure/Migrations/IMigrationPath.cs delete mode 100644 src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs delete mode 100644 src/Squidex.Infrastructure/Migrations/Migrator.cs delete mode 100644 src/Squidex.Infrastructure/NamedId.cs delete mode 100644 src/Squidex.Infrastructure/NamedId{T}.cs delete mode 100644 src/Squidex.Infrastructure/None.cs delete mode 100644 src/Squidex.Infrastructure/Orleans/ActivationLimit.cs delete mode 100644 src/Squidex.Infrastructure/Orleans/GrainBase.cs delete mode 100644 src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs delete mode 100644 src/Squidex.Infrastructure/Orleans/GrainState.cs delete mode 100644 src/Squidex.Infrastructure/Orleans/ILockGrain.cs delete mode 100644 src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs delete mode 100644 src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs delete mode 100644 src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs delete mode 100644 src/Squidex.Infrastructure/Orleans/J{T}.cs delete mode 100644 src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs delete mode 100644 src/Squidex.Infrastructure/Orleans/LockGrain.cs delete mode 100644 src/Squidex.Infrastructure/Orleans/LoggingFilter.cs delete mode 100644 src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs delete mode 100644 src/Squidex.Infrastructure/Plugins/PluginManager.cs delete mode 100644 src/Squidex.Infrastructure/Queries/ClrFilter.cs delete mode 100644 src/Squidex.Infrastructure/Queries/ClrValue.cs delete mode 100644 src/Squidex.Infrastructure/Queries/CompareFilter.cs delete mode 100644 src/Squidex.Infrastructure/Queries/FilterNode.cs delete mode 100644 src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs delete mode 100644 src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs delete mode 100644 src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs delete mode 100644 src/Squidex.Infrastructure/Queries/Json/QueryParser.cs delete mode 100644 src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs delete mode 100644 src/Squidex.Infrastructure/Queries/LogicalFilter.cs delete mode 100644 src/Squidex.Infrastructure/Queries/NegateFilter.cs delete mode 100644 src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs delete mode 100644 src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs delete mode 100644 src/Squidex.Infrastructure/Queries/Optimizer.cs delete mode 100644 src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs delete mode 100644 src/Squidex.Infrastructure/Queries/PropertyPath.cs delete mode 100644 src/Squidex.Infrastructure/Queries/Query.cs delete mode 100644 src/Squidex.Infrastructure/Queries/SortNode.cs delete mode 100644 src/Squidex.Infrastructure/Queries/TransformVisitor.cs delete mode 100644 src/Squidex.Infrastructure/RefToken.cs delete mode 100644 src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs delete mode 100644 src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs delete mode 100644 src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs delete mode 100644 src/Squidex.Infrastructure/Reflection/SimpleCopier.cs delete mode 100644 src/Squidex.Infrastructure/Reflection/SimpleMapper.cs delete mode 100644 src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs delete mode 100644 src/Squidex.Infrastructure/RetryWindow.cs delete mode 100644 src/Squidex.Infrastructure/Security/Extensions.cs delete mode 100644 src/Squidex.Infrastructure/Security/Permission.Part.cs delete mode 100644 src/Squidex.Infrastructure/Security/Permission.cs delete mode 100644 src/Squidex.Infrastructure/Security/PermissionSet.cs delete mode 100644 src/Squidex.Infrastructure/Squidex.Infrastructure.csproj delete mode 100644 src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs delete mode 100644 src/Squidex.Infrastructure/States/IStore.cs delete mode 100644 src/Squidex.Infrastructure/States/IStreamNameResolver.cs delete mode 100644 src/Squidex.Infrastructure/States/InconsistentStateException.cs delete mode 100644 src/Squidex.Infrastructure/States/Persistence.cs delete mode 100644 src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs delete mode 100644 src/Squidex.Infrastructure/States/Store.cs delete mode 100644 src/Squidex.Infrastructure/StringExtensions.cs delete mode 100644 src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs delete mode 100644 src/Squidex.Infrastructure/Tasks/AsyncLock.cs delete mode 100644 src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs delete mode 100644 src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs delete mode 100644 src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs delete mode 100644 src/Squidex.Infrastructure/Tasks/TaskExtensions.cs delete mode 100644 src/Squidex.Infrastructure/Tasks/TaskHelper.cs delete mode 100644 src/Squidex.Infrastructure/Timers/CompletionTimer.cs delete mode 100644 src/Squidex.Infrastructure/Translations/DeepLTranslator.cs delete mode 100644 src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs delete mode 100644 src/Squidex.Infrastructure/Translations/ITranslator.cs delete mode 100644 src/Squidex.Infrastructure/Translations/NoopTranslator.cs delete mode 100644 src/Squidex.Infrastructure/Translations/Translation.cs delete mode 100644 src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs delete mode 100644 src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs delete mode 100644 src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs delete mode 100644 src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs delete mode 100644 src/Squidex.Infrastructure/Validation/Validate.cs delete mode 100644 src/Squidex.Infrastructure/Validation/ValidationError.cs delete mode 100644 src/Squidex.Infrastructure/Validation/ValidationException.cs delete mode 100644 src/Squidex.Shared/Permissions.cs delete mode 100644 src/Squidex.Shared/Squidex.Shared.csproj delete mode 100644 src/Squidex.Shared/Users/ClientUser.cs delete mode 100644 src/Squidex.Shared/Users/IUserResolver.cs delete mode 100644 src/Squidex.Shared/Users/UserExtensions.cs delete mode 100644 src/Squidex.Web/ApiController.cs delete mode 100644 src/Squidex.Web/ApiExceptionFilterAttribute.cs delete mode 100644 src/Squidex.Web/AssetRequestSizeLimitAttribute.cs delete mode 100644 src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs delete mode 100644 src/Squidex.Web/ContextProvider.cs delete mode 100644 src/Squidex.Web/Deferred.cs delete mode 100644 src/Squidex.Web/EntityCreatedDto.cs delete mode 100644 src/Squidex.Web/ExposedValues.cs delete mode 100644 src/Squidex.Web/Extensions.cs delete mode 100644 src/Squidex.Web/FileCallbackResult.cs delete mode 100644 src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs delete mode 100644 src/Squidex.Web/PermissionExtensions.cs delete mode 100644 src/Squidex.Web/Pipeline/ApiCostsFilter.cs delete mode 100644 src/Squidex.Web/Pipeline/AppResolver.cs delete mode 100644 src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs delete mode 100644 src/Squidex.Web/Resource.cs delete mode 100644 src/Squidex.Web/ResourceLink.cs delete mode 100644 src/Squidex.Web/Services/UrlGenerator.cs delete mode 100644 src/Squidex.Web/Squidex.Web.csproj delete mode 100644 src/Squidex.Web/UrlHelperExtensions.cs delete mode 100644 src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs delete mode 100644 src/Squidex/Areas/Api/Config/OpenApi/OpenApiExtensions.cs delete mode 100644 src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs delete mode 100644 src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs delete mode 100644 src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs delete mode 100644 src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs delete mode 100644 src/Squidex/Areas/Api/Controllers/Users/UsersController.cs delete mode 100644 src/Squidex/Areas/Api/Startup.cs delete mode 100644 src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs delete mode 100644 src/Squidex/Areas/Frontend/Startup.cs delete mode 100644 src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.pfx delete mode 100644 src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.snk delete mode 100644 src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs delete mode 100644 src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs delete mode 100644 src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs delete mode 100644 src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs delete mode 100644 src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs delete mode 100644 src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs delete mode 100644 src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs delete mode 100644 src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs delete mode 100644 src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs delete mode 100644 src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs delete mode 100644 src/Squidex/Areas/IdentityServer/Startup.cs delete mode 100644 src/Squidex/Areas/IdentityServer/Views/Extensions.cs delete mode 100644 src/Squidex/Areas/OrleansDashboard/Startup.cs delete mode 100644 src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs delete mode 100644 src/Squidex/Areas/Portal/Startup.cs delete mode 100644 src/Squidex/Config/Authentication/AuthenticationServices.cs delete mode 100644 src/Squidex/Config/Authentication/GithubAuthenticationServices.cs delete mode 100644 src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs delete mode 100644 src/Squidex/Config/Authentication/GoogleHandler.cs delete mode 100644 src/Squidex/Config/Authentication/IdentityServerServices.cs delete mode 100644 src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs delete mode 100644 src/Squidex/Config/Authentication/MicrosoftHandler.cs delete mode 100644 src/Squidex/Config/Authentication/OidcServices.cs delete mode 100644 src/Squidex/Config/Domain/AssetServices.cs delete mode 100644 src/Squidex/Config/Domain/EntitiesServices.cs delete mode 100644 src/Squidex/Config/Domain/EventPublishersServices.cs delete mode 100644 src/Squidex/Config/Domain/EventStoreServices.cs delete mode 100644 src/Squidex/Config/Domain/InfrastructureServices.cs delete mode 100644 src/Squidex/Config/Domain/LoggingExtensions.cs delete mode 100644 src/Squidex/Config/Domain/LoggingServices.cs delete mode 100644 src/Squidex/Config/Domain/RuleServices.cs delete mode 100644 src/Squidex/Config/Domain/SerializationServices.cs delete mode 100644 src/Squidex/Config/Domain/StoreServices.cs delete mode 100644 src/Squidex/Config/Domain/SubscriptionServices.cs delete mode 100644 src/Squidex/Config/Logging.cs delete mode 100644 src/Squidex/Config/Orleans/OrleansServices.cs delete mode 100644 src/Squidex/Config/Startup/BackgroundHost.cs delete mode 100644 src/Squidex/Config/Startup/InitializerHost.cs delete mode 100644 src/Squidex/Config/Startup/MigrationRebuilderHost.cs delete mode 100644 src/Squidex/Config/Startup/MigratorHost.cs delete mode 100644 src/Squidex/Config/Startup/SafeHostedService.cs delete mode 100644 src/Squidex/Config/Web/WebExtensions.cs delete mode 100644 src/Squidex/Config/Web/WebServices.cs delete mode 100644 src/Squidex/Dockerfile delete mode 100644 src/Squidex/Pipeline/OpenApi/NSwagHelper.cs delete mode 100644 src/Squidex/Pipeline/Plugins/PluginExtensions.cs delete mode 100644 src/Squidex/Pipeline/Plugins/PluginLoaders.cs delete mode 100644 src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs delete mode 100644 src/Squidex/Pipeline/Squid/SquidMiddleware.cs delete mode 100644 src/Squidex/Program.cs delete mode 100644 src/Squidex/Squidex.csproj delete mode 100644 src/Squidex/WebStartup.cs delete mode 100644 src/Squidex/app-config/webpack.config.js delete mode 100644 src/Squidex/package-lock.json delete mode 100644 src/Squidex/package.json delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerGrainTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/JsonHelper.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs delete mode 100644 tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs delete mode 100644 tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj delete mode 100644 tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/GuardTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/LanguageTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/NamedIdTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Orleans/LockGrainTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/RefTokenTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj delete mode 100644 tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs delete mode 100644 tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs delete mode 100644 tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs delete mode 100644 tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs delete mode 100644 tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs delete mode 100644 tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs delete mode 100644 tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj delete mode 100644 tools/GenerateLanguages/GenerateLanguages.csproj delete mode 100644 tools/LoadTest/LoadTest.csproj delete mode 100644 tools/Migrate_00/Migrate_00.csproj delete mode 100644 tools/Migrate_01/Migrate_01.csproj delete mode 100644 tools/Migrate_01/MigrationPath.cs delete mode 100644 tools/Migrate_01/Migrations/AddPatterns.cs delete mode 100644 tools/Migrate_01/Migrations/ConvertEventStore.cs delete mode 100644 tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs delete mode 100644 tools/Migrate_01/RebuildRunner.cs diff --git a/.dockerignore b/.dockerignore index 9cb49913f..6ec863bb3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,19 +8,19 @@ .git # Build results -bin/ -build/ -obj/ -out/ -publish/ +**/bin +**/build +**/obj +**/out +**/publish # Test Output -_test-output/ +**/_test-output # NodeJS -node_modules/ +**/node_modules -src/Squidex/Assets/*.* +backend/src/Squidex/Assets/*.* **/appsettings.Development.json **/appsettings.Production.json diff --git a/.drone.yml b/.drone.yml index 802ce0459..3e84af743 100644 --- a/.drone.yml +++ b/.drone.yml @@ -63,9 +63,9 @@ steps: - name: build_binaries image: docker commands: - - docker build -t squidex-build-image -f Dockerfile.build --build-arg SQUIDEX__VERSION=$${DRONE_TAG} . + - docker build -t squidex-build-image -f Dockerfile --build-arg SQUIDEX__VERSION=$${DRONE_TAG} . - docker create --name squidex-build-container squidex-build-image - - docker cp squidex-build-container:/out /build + - docker cp squidex-build-container:/app /build volumes: - name: build path: /build @@ -117,8 +117,10 @@ steps: - name: cleanup-build image: docker commands: - - docker rm squidex-build-container - - docker rmi squidex-build-image + - docker rm squidex-backend-container + - docker rm squidex-frontend-container + - docker rmi squidex-backend-image + - docker rmi squidex-frontend-image volumes: - name: docker1 path: /var/run/docker.sock @@ -141,8 +143,10 @@ steps: - name: docker2 path: /var/lib/docker when: - status: - - failure + event: + - push + branch: + - cleaned volumes: - name: build diff --git a/.gitignore b/.gitignore index 668986e23..f971dec28 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ bin/ build/ obj/ +out/ publish/ # Test Output @@ -21,9 +22,6 @@ _test-output/ # NodeJS node_modules/ -# Scripts (should be copied from node_modules on build) -**/wwwroot/scripts/**/*.* - /src/Squidex/Assets/ appsettings.Development.json diff --git a/Dockerfile b/Dockerfile index 0626441cb..8f12d0c46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,33 +1,21 @@ # -# Stage 1, Prebuild +# Stage 1, Build Backend # -FROM squidex/dotnet:2.2-sdk-chromium-phantomjs-node as builder +FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster as backend ARG SQUIDEX__VERSION=1.0.0 WORKDIR /src -# Copy Node project files. -COPY src/Squidex/package*.json /tmp/ - -# Install Node packages -RUN cd /tmp && npm install --loglevel=error - # Copy nuget project files. -COPY /**/**/*.csproj /tmp/ +COPY backend/**/**/*.csproj /tmp/ # Copy nuget.config for package sources. -COPY NuGet.Config /tmp/ +COPY backend/NuGet.Config /tmp/ # Install nuget packages RUN bash -c 'pushd /tmp; for p in *.csproj; do dotnet restore $p --verbosity quiet; true; done; popd' -COPY . . - -# Build Frontend -RUN cp -a /tmp/node_modules src/Squidex/ \ - && cd src/Squidex \ - && npm run test:coverage \ - && npm run build +COPY backend . # Test Backend RUN dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj --filter Category!=Dependencies \ @@ -37,27 +25,45 @@ RUN dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests. && dotnet test tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj # Publish -RUN dotnet publish src/Squidex/Squidex.csproj --output /out/alpine --configuration Release -r alpine.3.7-x64 -p:version=$SQUIDEX__VERSION +RUN dotnet publish src/Squidex/Squidex.csproj --output /build/ --configuration Release -p:version=$SQUIDEX__VERSION + + +# +# Stage 2, Build Frontend +# +FROM buildkite/puppeteer:latest as frontend + +WORKDIR /src + +# Copy Node project files. +COPY frontend/package*.json /tmp/ + +# Install Node packages +RUN cd /tmp && npm install --loglevel=error + +COPY frontend . + +# Build Frontend +RUN cp -a /tmp/node_modules . \ + && npm run test:coverage \ + && npm run build + +RUN cp -a build /build/ + # -# Stage 2, Build runtime +# Stage 3, Build runtime # -FROM mcr.microsoft.com/dotnet/core/runtime-deps:2.2-alpine3.8 +FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-buster-slim # Default AspNetCore directory WORKDIR /app -# add libuv & curl -RUN apk update \ - && apk add --no-cache libc6-compat \ - && apk add --no-cache libuv \ - && apk add --no-cache curl \ - && ln -s /usr/lib/libuv.so.1 /usr/lib/libuv.so - -# Copy from build stage -COPY --from=builder /out/alpine . +# Copy from build stages +COPY --from=backend /build/ . +COPY --from=frontend /build/ wwwroot/build/ EXPOSE 80 EXPOSE 11111 -ENTRYPOINT ["./Squidex"] \ No newline at end of file +ENTRYPOINT ["dotnet", "Squidex.dll"] \ No newline at end of file diff --git a/Dockerfile.build b/Dockerfile.build deleted file mode 100644 index c2002ae5c..000000000 --- a/Dockerfile.build +++ /dev/null @@ -1,37 +0,0 @@ -FROM squidex/dotnet:2.2-sdk-chromium-phantomjs-node as builder - -ARG SQUIDEX__VERSION=1.0.0 - -WORKDIR /src - -# Copy Node project files. -COPY src/Squidex/package*.json /tmp/ - -# Install Node packages -RUN cd /tmp && npm install --loglevel=error - -# Copy Dotnet project files. -COPY /**/**/*.csproj /tmp/ -# Copy nuget.config for package sources. -COPY NuGet.Config /tmp/ - -# Install Dotnet packages -RUN bash -c 'pushd /tmp; for p in *.csproj; do dotnet restore $p --verbosity quiet; true; done; popd' - -COPY . . - -# Build Frontend -RUN cp -a /tmp/node_modules src/Squidex/ \ - && cd src/Squidex \ - && npm run test:coverage \ - && npm run build - -# Test Backend -RUN dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj --filter Category!=Dependencies \ - && dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \ - && dotnet test tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj \ - && dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj \ - && dotnet test tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj - -# Publish -RUN dotnet publish src/Squidex/Squidex.csproj --output /out/ --configuration Release -p:version=$SQUIDEX__VERSION \ No newline at end of file diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 100644 index 000000000..7afe7ca48 --- /dev/null +++ b/backend/.editorconfig @@ -0,0 +1,16 @@ +[*.cs] + +# CS8618: Non-nullable field is uninitialized. Consider declaring as nullable. +dotnet_diagnostic.CS8618.severity = none + +# SA1011: Closing square brackets should be spaced correctly +dotnet_diagnostic.SA1011.severity = none + +# IDE0066: Convert switch statement to expression +csharp_style_prefer_switch_expression = false:suggestion + +# IDE0010: Add missing cases +dotnet_diagnostic.IDE0010.severity = none + +# IDE0063: Use simple 'using' statement +csharp_prefer_simple_using_statement = false:suggestion diff --git a/NuGet.Config b/backend/NuGet.Config similarity index 100% rename from NuGet.Config rename to backend/NuGet.Config diff --git a/Squidex.ruleset b/backend/Squidex.ruleset similarity index 100% rename from Squidex.ruleset rename to backend/Squidex.ruleset diff --git a/Squidex.sln b/backend/Squidex.sln similarity index 100% rename from Squidex.sln rename to backend/Squidex.sln diff --git a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs new file mode 100644 index 000000000..b1ec2c2d0 --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs @@ -0,0 +1,135 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using Algolia.Search; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using AlgoliaIndex = Algolia.Search.Index; + +#pragma warning disable IDE0059 // Value assigned to symbol is never used + +namespace Squidex.Extensions.Actions.Algolia +{ + public sealed class AlgoliaActionHandler : RuleActionHandler + { + private readonly ClientPool<(string AppId, string ApiKey, string IndexName), AlgoliaIndex> clients; + + public AlgoliaActionHandler(RuleEventFormatter formatter) + : base(formatter) + { + clients = new ClientPool<(string AppId, string ApiKey, string IndexName), AlgoliaIndex>(key => + { + var client = new AlgoliaClient(key.AppId, key.ApiKey); + + return client.InitIndex(key.IndexName); + }); + } + + protected override (string Description, AlgoliaJob Data) CreateJob(EnrichedEvent @event, AlgoliaAction action) + { + if (@event is EnrichedContentEvent contentEvent) + { + var contentId = contentEvent.Id.ToString(); + + var ruleDescription = string.Empty; + var ruleJob = new AlgoliaJob + { + AppId = action.AppId, + ApiKey = action.ApiKey, + ContentId = contentId, + IndexName = Format(action.IndexName, @event) + }; + + if (contentEvent.Type == EnrichedContentEventType.Deleted || + contentEvent.Type == EnrichedContentEventType.Unpublished) + { + ruleDescription = $"Delete entry from Algolia index: {action.IndexName}"; + } + else + { + ruleDescription = $"Add entry to Algolia index: {action.IndexName}"; + + JObject json; + try + { + string jsonString; + + if (!string.IsNullOrEmpty(action.Document)) + { + jsonString = Format(action.Document, @event)?.Trim(); + } + else + { + jsonString = ToJson(contentEvent); + } + + json = JObject.Parse(jsonString); + } + catch (Exception ex) + { + json = new JObject(new JProperty("error", $"Invalid JSON: {ex.Message}")); + } + + ruleJob.Content = json; + ruleJob.Content["objectID"] = contentId; + } + + return (ruleDescription, ruleJob); + } + + return ("Ignore", new AlgoliaJob()); + } + + protected override async Task ExecuteJobAsync(AlgoliaJob job, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(job.AppId)) + { + return Result.Ignored(); + } + + var index = clients.GetClient((job.AppId, job.ApiKey, job.IndexName)); + + try + { + if (job.Content != null) + { + var response = await index.PartialUpdateObjectAsync(job.Content, true, ct); + + return Result.Success(response.ToString(Formatting.Indented)); + } + else + { + var response = await index.DeleteObjectAsync(job.ContentId, ct); + + return Result.Success(response.ToString(Formatting.Indented)); + } + } + catch (AlgoliaException ex) + { + return Result.Failed(ex); + } + } + } + + public sealed class AlgoliaJob + { + public string AppId { get; set; } + + public string ApiKey { get; set; } + + public string ContentId { get; set; } + + public string IndexName { get; set; } + + public JObject Content { get; set; } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs rename to backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs diff --git a/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs rename to backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs diff --git a/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/ClientPool.cs b/backend/extensions/Squidex.Extensions/Actions/ClientPool.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/ClientPool.cs rename to backend/extensions/Squidex.Extensions/Actions/ClientPool.cs diff --git a/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs diff --git a/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs rename to backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs diff --git a/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs rename to backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs diff --git a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs rename to backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs diff --git a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs b/backend/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Email/EmailAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs diff --git a/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs rename to backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs diff --git a/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs new file mode 100644 index 000000000..f971a49b7 --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs @@ -0,0 +1,70 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Infrastructure; + +namespace Squidex.Extensions.Actions.Fastly +{ + public sealed class FastlyActionHandler : RuleActionHandler + { + private const string Description = "Purge key in fastly"; + + private readonly IHttpClientFactory httpClientFactory; + + public FastlyActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) + : base(formatter) + { + Guard.NotNull(httpClientFactory); + + this.httpClientFactory = httpClientFactory; + } + + protected override (string Description, FastlyJob Data) CreateJob(EnrichedEvent @event, FastlyAction action) + { + var id = @event is IEnrichedEntityEvent entityEvent ? entityEvent.Id.ToString() : string.Empty; + + var ruleJob = new FastlyJob + { + Key = id, + FastlyApiKey = action.ApiKey, + FastlyServiceID = action.ServiceId + }; + + return (Description, ruleJob); + } + + protected override async Task ExecuteJobAsync(FastlyJob job, CancellationToken ct = default) + { + using (var httpClient = httpClientFactory.CreateClient()) + { + httpClient.Timeout = TimeSpan.FromSeconds(2); + + var requestUrl = $"https://api.fastly.com/service/{job.FastlyServiceID}/purge/{job.Key}"; + var request = new HttpRequestMessage(HttpMethod.Post, requestUrl); + + request.Headers.Add("Fastly-Key", job.FastlyApiKey); + + return await httpClient.OneWayRequestAsync(request, ct: ct); + } + } + } + + public sealed class FastlyJob + { + public string FastlyApiKey { get; set; } + + public string FastlyServiceID { get; set; } + + public string Key { get; set; } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/HttpHelper.cs b/backend/extensions/Squidex.Extensions/Actions/HttpHelper.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/HttpHelper.cs rename to backend/extensions/Squidex.Extensions/Actions/HttpHelper.cs diff --git a/extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs diff --git a/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs rename to backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs diff --git a/extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs rename to backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs diff --git a/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs rename to backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs diff --git a/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs diff --git a/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs rename to backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs diff --git a/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs diff --git a/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs rename to backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs diff --git a/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs new file mode 100644 index 000000000..0815f3baa --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Infrastructure; + +namespace Squidex.Extensions.Actions.Slack +{ + public sealed class SlackActionHandler : RuleActionHandler + { + private const string Description = "Send message to slack"; + + private readonly IHttpClientFactory httpClientFactory; + + public SlackActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) + : base(formatter) + { + Guard.NotNull(httpClientFactory); + + this.httpClientFactory = httpClientFactory; + } + + protected override (string Description, SlackJob Data) CreateJob(EnrichedEvent @event, SlackAction action) + { + var body = new { text = Format(action.Text, @event) }; + + var ruleJob = new SlackJob + { + RequestUrl = action.WebhookUrl.ToString(), + RequestBody = ToJson(body) + }; + + return (Description, ruleJob); + } + + protected override async Task ExecuteJobAsync(SlackJob job, CancellationToken ct = default) + { + using (var httpClient = httpClientFactory.CreateClient()) + { + httpClient.Timeout = TimeSpan.FromSeconds(2); + + var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) + { + Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") + }; + + return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); + } + } + } + + public sealed class SlackJob + { + public string RequestUrl { get; set; } + + public string RequestBody { get; set; } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs b/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs new file mode 100644 index 000000000..ddd91d9eb --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using CoreTweet; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Infrastructure; + +namespace Squidex.Extensions.Actions.Twitter +{ + public sealed class TweetActionHandler : RuleActionHandler + { + private const string Description = "Send a tweet"; + + private readonly TwitterOptions twitterOptions; + + public TweetActionHandler(RuleEventFormatter formatter, IOptions twitterOptions) + : base(formatter) + { + Guard.NotNull(twitterOptions); + + this.twitterOptions = twitterOptions.Value; + } + + protected override (string Description, TweetJob Data) CreateJob(EnrichedEvent @event, TweetAction action) + { + var ruleJob = new TweetJob + { + Text = Format(action.Text, @event), + AccessToken = action.AccessToken, + AccessSecret = action.AccessSecret + }; + + return (Description, ruleJob); + } + + protected override async Task ExecuteJobAsync(TweetJob job, CancellationToken ct = default) + { + var tokens = Tokens.Create( + twitterOptions.ClientId, + twitterOptions.ClientSecret, + job.AccessToken, + job.AccessSecret); + + var request = new Dictionary + { + ["status"] = job.Text + }; + + await tokens.Statuses.UpdateAsync(request, ct); + + return Result.Success($"Tweeted: {job.Text}"); + } + } + + public sealed class TweetJob + { + public string AccessToken { get; set; } + + public string AccessSecret { get; set; } + + public string Text { get; set; } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs b/backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs rename to backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs diff --git a/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs new file mode 100644 index 000000000..ccfa6f308 --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs @@ -0,0 +1,86 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Infrastructure; + +namespace Squidex.Extensions.Actions.Webhook +{ + public sealed class WebhookActionHandler : RuleActionHandler + { + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(2); + private readonly IHttpClientFactory httpClientFactory; + + public WebhookActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) + : base(formatter) + { + Guard.NotNull(httpClientFactory); + + this.httpClientFactory = httpClientFactory; + } + + protected override (string Description, WebhookJob Data) CreateJob(EnrichedEvent @event, WebhookAction action) + { + string requestBody; + + if (!string.IsNullOrEmpty(action.Payload)) + { + requestBody = Format(action.Payload, @event); + } + else + { + requestBody = ToEnvelopeJson(@event); + } + + var requestUrl = Format(action.Url, @event); + + var ruleDescription = $"Send event to webhook '{requestUrl}'"; + var ruleJob = new WebhookJob + { + RequestUrl = Format(action.Url.ToString(), @event), + RequestSignature = $"{requestBody}{action.SharedSecret}".Sha256Base64(), + RequestBody = requestBody + }; + + return (ruleDescription, ruleJob); + } + + protected override async Task ExecuteJobAsync(WebhookJob job, CancellationToken ct = default) + { + using (var httpClient = httpClientFactory.CreateClient()) + { + httpClient.Timeout = DefaultTimeout; + + var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) + { + Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") + }; + + request.Headers.Add("X-Signature", job.RequestSignature); + request.Headers.Add("X-Application", "Squidex Webhook"); + request.Headers.Add("User-Agent", "Squidex Webhook"); + + return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); + } + } + } + + public sealed class WebhookJob + { + public string RequestUrl { get; set; } + + public string RequestSignature { get; set; } + + public string RequestBody { get; set; } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs diff --git a/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs b/backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs rename to backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs diff --git a/extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs b/backend/extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs similarity index 100% rename from extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs rename to backend/extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs diff --git a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj new file mode 100644 index 000000000..cca01d821 --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj @@ -0,0 +1,31 @@ + + + netcoreapp3.0 + 8.0 + + + + + + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs new file mode 100644 index 000000000..1a1b64193 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class AppClient : Named + { + public string Role { get; } + + public string Secret { get; } + + public AppClient(string name, string secret, string role) + : base(name) + { + Guard.NotNullOrEmpty(secret); + Guard.NotNullOrEmpty(role); + + Role = role; + + Secret = secret; + } + + [Pure] + public AppClient Update(string newRole) + { + Guard.NotNullOrEmpty(newRole); + + return new AppClient(Name, Secret, newRole); + } + + [Pure] + public AppClient Rename(string newName) + { + Guard.NotNullOrEmpty(newName); + + return new AppClient(newName, Secret, Role); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs new file mode 100644 index 000000000..f9d7c83a0 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs @@ -0,0 +1,90 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class AppClients : ArrayDictionary + { + public static readonly AppClients Empty = new AppClients(); + + private AppClients() + { + } + + public AppClients(KeyValuePair[] items) + : base(items) + { + } + + [Pure] + public AppClients Revoke(string id) + { + Guard.NotNullOrEmpty(id); + + return new AppClients(Without(id)); + } + + [Pure] + public AppClients Add(string id, AppClient client) + { + Guard.NotNullOrEmpty(id); + Guard.NotNull(client); + + if (ContainsKey(id)) + { + throw new ArgumentException("Id already exists.", nameof(id)); + } + + return new AppClients(With(id, client)); + } + + [Pure] + public AppClients Add(string id, string secret) + { + Guard.NotNullOrEmpty(id); + + if (ContainsKey(id)) + { + throw new ArgumentException("Id already exists.", nameof(id)); + } + + return new AppClients(With(id, new AppClient(id, secret, Role.Editor))); + } + + [Pure] + public AppClients Rename(string id, string newName) + { + Guard.NotNullOrEmpty(id); + + if (!TryGetValue(id, out var client)) + { + return this; + } + + return new AppClients(With(id, client.Rename(newName))); + } + + [Pure] + public AppClients Update(string id, string role) + { + Guard.NotNullOrEmpty(id); + + if (!TryGetValue(id, out var client)) + { + return this; + } + + return new AppClients(With(id, client.Update(role))); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs new file mode 100644 index 000000000..5d0a81cac --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs @@ -0,0 +1,45 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class AppContributors : ArrayDictionary + { + public static readonly AppContributors Empty = new AppContributors(); + + private AppContributors() + { + } + + public AppContributors(KeyValuePair[] items) + : base(items) + { + } + + [Pure] + public AppContributors Assign(string contributorId, string role) + { + Guard.NotNullOrEmpty(contributorId); + Guard.NotNullOrEmpty(role); + + return new AppContributors(With(contributorId, role)); + } + + [Pure] + public AppContributors Remove(string contributorId) + { + Guard.NotNullOrEmpty(contributorId); + + return new AppContributors(Without(contributorId)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs new file mode 100644 index 000000000..3d8c59d2c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class AppImage + { + public string MimeType { get; } + + public string Etag { get; } + + public AppImage(string mimeType, string? etag = null) + { + Guard.NotNullOrEmpty(mimeType); + + MimeType = mimeType; + + if (string.IsNullOrWhiteSpace(etag)) + { + Etag = RandomHash.Simple(); + } + else + { + Etag = etag; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs new file mode 100644 index 000000000..967b891e7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class AppPattern : Named + { + public string Pattern { get; } + + public string? Message { get; } + + public AppPattern(string name, string pattern, string? message = null) + : base(name) + { + Guard.NotNullOrEmpty(pattern); + + Pattern = pattern; + + Message = message; + } + + [Pure] + public AppPattern Update(string newName, string newPattern, string? newMessage) + { + return new AppPattern(newName, newPattern, newMessage); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs new file mode 100644 index 000000000..e31daa5e7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class AppPatterns : ArrayDictionary + { + public static readonly AppPatterns Empty = new AppPatterns(); + + private AppPatterns() + { + } + + public AppPatterns(KeyValuePair[] items) + : base(items) + { + } + + [Pure] + public AppPatterns Remove(Guid id) + { + return new AppPatterns(Without(id)); + } + + [Pure] + public AppPatterns Add(Guid id, string name, string pattern, string? message) + { + var newPattern = new AppPattern(name, pattern, message); + + if (ContainsKey(id)) + { + throw new ArgumentException("Id already exists.", nameof(id)); + } + + return new AppPatterns(With(id, newPattern)); + } + + [Pure] + public AppPatterns Update(Guid id, string name, string pattern, string? message) + { + Guard.NotNullOrEmpty(name); + Guard.NotNullOrEmpty(pattern); + + if (!TryGetValue(id, out var appPattern)) + { + return this; + } + + return new AppPatterns(With(id, appPattern.Update(name, pattern, message))); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs new file mode 100644 index 000000000..c3ef0856e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class AppPlan + { + public RefToken Owner { get; } + + public string PlanId { get; } + + public AppPlan(RefToken owner, string planId) + { + Guard.NotNull(owner); + Guard.NotNullOrEmpty(planId); + + Owner = owner; + + PlanId = planId; + } + + public static AppPlan? Build(RefToken owner, string planId) + { + if (planId == null) + { + return null; + } + else + { + return new AppPlan(owner, planId); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs new file mode 100644 index 000000000..dfca9aa3b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Newtonsoft.Json; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Core.Apps.Json +{ + public class JsonAppPattern + { + [JsonProperty] + public string Name { get; set; } + + [JsonProperty] + public string Pattern { get; set; } + + [JsonProperty] + public string? Message { get; set; } + + public JsonAppPattern() + { + } + + public JsonAppPattern(AppPattern pattern) + { + SimpleMapper.Map(pattern, this); + } + + public AppPattern ToPattern() + { + return new AppPattern(Name, Pattern, Message); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs new file mode 100644 index 000000000..d1ac0c353 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Newtonsoft.Json; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps.Json +{ + public sealed class JsonLanguagesConfig + { + [JsonProperty] + public Dictionary Languages { get; set; } + + [JsonProperty] + public Language? Master { get; set; } + + public JsonLanguagesConfig() + { + } + + public JsonLanguagesConfig(LanguagesConfig value) + { + Languages = new Dictionary(value.Count); + + foreach (LanguageConfig config in value) + { + Languages.Add(config.Language, new JsonLanguageConfig(config)); + } + + Master = value.Master?.Language; + } + + public LanguagesConfig ToConfig() + { + var languagesConfig = new LanguageConfig[Languages?.Count ?? 0]; + + if (Languages != null) + { + var i = 0; + + foreach (var config in Languages) + { + languagesConfig[i++] = config.Value.ToConfig(config.Key); + } + } + + var result = LanguagesConfig.Build(languagesConfig); + + if (Master != null) + { + result = result.MakeMaster(Master); + } + + return result; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs new file mode 100644 index 000000000..1a80e62f0 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class LanguageConfig : IFieldPartitionItem + { + private readonly Language language; + private readonly Language[] languageFallbacks; + + public bool IsOptional { get; } + + public Language Language + { + get { return language; } + } + + public IEnumerable LanguageFallbacks + { + get { return languageFallbacks; } + } + + string IFieldPartitionItem.Key + { + get { return language.Iso2Code; } + } + + string IFieldPartitionItem.Name + { + get { return language.EnglishName; } + } + + IEnumerable IFieldPartitionItem.Fallback + { + get { return LanguageFallbacks.Select(x => x.Iso2Code); } + } + + public LanguageConfig(Language language, bool isOptional = false, IEnumerable? fallback = null) + : this(language, isOptional, fallback?.ToArray()) + { + } + + public LanguageConfig(Language language, bool isOptional = false, params Language[]? fallback) + { + Guard.NotNull(language); + + IsOptional = isOptional; + + this.language = language; + this.languageFallbacks = fallback ?? Array.Empty(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs new file mode 100644 index 000000000..069c640ec --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs @@ -0,0 +1,179 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Linq; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class LanguagesConfig : IFieldPartitioning + { + public static readonly LanguagesConfig English = Build(Language.EN); + + private readonly ArrayDictionary languages; + private readonly LanguageConfig master; + + public LanguageConfig Master + { + get { return master; } + } + + IFieldPartitionItem IFieldPartitioning.Master + { + get { return master; } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return languages.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return languages.Values.GetEnumerator(); + } + + public int Count + { + get { return languages.Count; } + } + + private LanguagesConfig(ArrayDictionary languages, LanguageConfig master, bool checkMaster = true) + { + if (checkMaster) + { + this.master = master ?? throw new InvalidOperationException("Config has no master language."); + } + + foreach (var languageConfig in languages.Values) + { + foreach (var fallback in languageConfig.LanguageFallbacks) + { + if (!languages.ContainsKey(fallback)) + { + var message = $"Config for language '{languageConfig.Language.Iso2Code}' contains unsupported fallback language '{fallback.Iso2Code}'"; + + throw new InvalidOperationException(message); + } + } + } + + this.languages = languages; + } + + public static LanguagesConfig Build(ICollection configs) + { + Guard.NotNull(configs); + + return new LanguagesConfig(configs.ToArrayDictionary(x => x.Language), configs.FirstOrDefault()); + } + + public static LanguagesConfig Build(params LanguageConfig[] configs) + { + return Build(configs?.ToList()!); + } + + public static LanguagesConfig Build(params Language[] languages) + { + return Build(languages?.Select(x => new LanguageConfig(x)).ToList()!); + } + + [Pure] + public LanguagesConfig MakeMaster(Language language) + { + Guard.NotNull(language); + + return new LanguagesConfig(languages, languages[language]); + } + + [Pure] + public LanguagesConfig Set(Language language, bool isOptional = false, IEnumerable? fallback = null) + { + Guard.NotNull(language); + + return Set(new LanguageConfig(language, isOptional, fallback)); + } + + [Pure] + public LanguagesConfig Set(LanguageConfig config) + { + Guard.NotNull(config); + + var newLanguages = + new ArrayDictionary(languages.With(config.Language, config)); + + var newMaster = Master?.Language == config.Language ? config : Master; + + return new LanguagesConfig(newLanguages, newMaster!); + } + + [Pure] + public LanguagesConfig Remove(Language language) + { + Guard.NotNull(language); + + var newLanguages = + languages.Values.Where(x => x.Language != language) + .Select(config => new LanguageConfig( + config.Language, + config.IsOptional, + config.LanguageFallbacks.Except(new[] { language }))) + .ToArrayDictionary(x => x.Language); + + var newMaster = + newLanguages.Values.FirstOrDefault(x => x.Language == Master.Language) ?? + newLanguages.Values.FirstOrDefault(); + + return new LanguagesConfig(newLanguages, newMaster); + } + + public bool Contains(Language language) + { + return language != null && languages.ContainsKey(language); + } + + public bool TryGetConfig(Language language, [MaybeNullWhen(false)] out LanguageConfig config) + { + return languages.TryGetValue(language, out config!); + } + + public bool TryGetItem(string key, [MaybeNullWhen(false)] out IFieldPartitionItem item) + { + if (Language.IsValidLanguage(key) && languages.TryGetValue(key, out var value)) + { + item = value; + + return true; + } + else + { + item = null!; + + return false; + } + } + + public PartitionResolver ToResolver() + { + return partitioning => + { + if (partitioning.Equals(Partitioning.Invariant)) + { + return InvariantPartitioning.Instance; + } + + return this; + }; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs new file mode 100644 index 000000000..297f23ec4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs @@ -0,0 +1,76 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using AllPermissions = Squidex.Shared.Permissions; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class Role : Named + { + public const string Editor = "Editor"; + public const string Developer = "Developer"; + public const string Owner = "Owner"; + public const string Reader = "Reader"; + + public PermissionSet Permissions { get; } + + public bool IsDefault + { + get { return Roles.IsDefault(this); } + } + + public Role(string name, PermissionSet permissions) + : base(name) + { + Guard.NotNull(permissions); + + Permissions = permissions; + } + + public Role(string name, params string[] permissions) + : this(name, new PermissionSet(permissions)) + { + } + + [Pure] + public Role Update(string[] permissions) + { + return new Role(Name, new PermissionSet(permissions)); + } + + public bool Equals(string name) + { + return name != null && name.Equals(Name, StringComparison.Ordinal); + } + + public Role ForApp(string app) + { + var result = new HashSet + { + AllPermissions.ForApp(AllPermissions.AppCommon, app) + }; + + if (Permissions.Any()) + { + var prefix = AllPermissions.ForApp(AllPermissions.App, app).Id; + + foreach (var permission in Permissions) + { + result.Add(new Permission(string.Concat(prefix, ".", permission.Id))); + } + } + + return new Role(Name, new PermissionSet(result)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs new file mode 100644 index 000000000..035a2bd79 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs @@ -0,0 +1,180 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Linq; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Security; +using Squidex.Shared; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class Roles + { + private readonly ArrayDictionary inner; + + public static readonly IReadOnlyDictionary Defaults = new Dictionary + { + [Role.Owner] = + new Role(Role.Owner, new PermissionSet( + Clean(Permissions.App))), + [Role.Reader] = + new Role(Role.Reader, new PermissionSet( + Clean(Permissions.AppAssetsRead), + Clean(Permissions.AppContentsRead))), + [Role.Editor] = + new Role(Role.Editor, new PermissionSet( + Clean(Permissions.AppAssets), + Clean(Permissions.AppContents), + Clean(Permissions.AppRolesRead), + Clean(Permissions.AppWorkflowsRead))), + [Role.Developer] = + new Role(Role.Developer, new PermissionSet( + Clean(Permissions.AppApi), + Clean(Permissions.AppAssets), + Clean(Permissions.AppContents), + Clean(Permissions.AppPatterns), + Clean(Permissions.AppRolesRead), + Clean(Permissions.AppRules), + Clean(Permissions.AppSchemas), + Clean(Permissions.AppWorkflows))) + }; + + public static readonly Roles Empty = new Roles(new ArrayDictionary()); + + public int CustomCount + { + get { return inner.Count; } + } + + public Role this[string name] + { + get { return inner[name]; } + } + + public IEnumerable Custom + { + get { return inner.Values; } + } + + public IEnumerable All + { + get { return inner.Values.Union(Defaults.Values); } + } + + private Roles(ArrayDictionary roles) + { + inner = roles; + } + + public Roles(IEnumerable> items) + { + inner = new ArrayDictionary(Cleaned(items)); + } + + [Pure] + public Roles Remove(string name) + { + return new Roles(inner.Without(name)); + } + + [Pure] + public Roles Add(string name) + { + var newRole = new Role(name); + + if (inner.ContainsKey(name)) + { + throw new ArgumentException("Name already exists.", nameof(name)); + } + + if (IsDefault(name)) + { + return this; + } + + return new Roles(inner.With(name, newRole)); + } + + [Pure] + public Roles Update(string name, params string[] permissions) + { + Guard.NotNullOrEmpty(name); + Guard.NotNull(permissions); + + if (!inner.TryGetValue(name, out var role)) + { + return this; + } + + return new Roles(inner.With(name, role.Update(permissions))); + } + + public static bool IsDefault(string role) + { + return role != null && Defaults.ContainsKey(role); + } + + public static bool IsDefault(Role role) + { + return role != null && Defaults.ContainsKey(role.Name); + } + + public bool ContainsCustom(string name) + { + return inner.ContainsKey(name); + } + + public bool Contains(string name) + { + return inner.ContainsKey(name) || Defaults.ContainsKey(name); + } + + public bool TryGet(string app, string name, [MaybeNullWhen(false)] out Role value) + { + Guard.NotNull(app, nameof(app)); + + if (Defaults.TryGetValue(name, out var role) || inner.TryGetValue(name, out role)) + { + value = role.ForApp(app); + return true; + } + + value = null!; + + return false; + } + + private static string Clean(string permission) + { + permission = Permissions.ForApp(permission).Id; + + var prefix = Permissions.ForApp(Permissions.App); + + if (permission.StartsWith(prefix.Id, StringComparison.OrdinalIgnoreCase)) + { + permission = permission.Substring(prefix.Id.Length); + } + + if (permission.Length == 0) + { + return Permission.Any; + } + + return permission.Substring(1); + } + + private static KeyValuePair[] Cleaned(IEnumerable> items) + { + return items.Where(x => !Defaults.ContainsKey(x.Key)).ToArray(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs new file mode 100644 index 000000000..67a3dbd54 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Comments +{ + public sealed class Comment + { + public Guid Id { get; } + + public Instant Time { get; } + + public RefToken User { get; } + + public string Text { get; } + + public Comment(Guid id, Instant time, RefToken user, string text) + { + Guard.NotEmpty(id); + Guard.NotNull(user); + Guard.NotNull(text); + + Id = id; + + Time = time; + Text = text; + + User = user; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs new file mode 100644 index 000000000..064700a36 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs @@ -0,0 +1,99 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public abstract class ContentData : Dictionary, IEquatable> where T : notnull + { + public IEnumerable> ValidValues + { + get { return this.Where(x => x.Value != null); } + } + + protected ContentData(IEqualityComparer comparer) + : base(comparer) + { + } + + protected ContentData(int capacity, IEqualityComparer comparer) + : base(capacity, comparer) + { + } + + protected static TResult MergeTo(TResult target, params TResult[] sources) where TResult : ContentData + { + Guard.NotEmpty(sources); + + if (sources.Length == 1 || sources.Skip(1).All(x => ReferenceEquals(x, sources[0]))) + { + return sources[0]; + } + + foreach (var source in sources) + { + foreach (var otherValue in source) + { + if (otherValue.Value != null) + { + var fieldValue = target.GetOrAdd(otherValue.Key, x => new ContentFieldData()); + + if (fieldValue != null) + { + foreach (var value in otherValue.Value) + { + fieldValue[value.Key] = value.Value; + } + } + } + } + } + + return target; + } + + protected static TResult Clean(TResult source, TResult target) where TResult : ContentData + { + foreach (var fieldValue in source.ValidValues) + { + var resultValue = new ContentFieldData(); + + foreach (var partitionValue in fieldValue.Value.Where(x => x.Value.Type != JsonValueType.Null)) + { + resultValue[partitionValue.Key] = partitionValue.Value; + } + + if (resultValue.Count > 0) + { + target[fieldValue.Key] = resultValue; + } + } + + return target; + } + + public override bool Equals(object? obj) + { + return Equals(obj as ContentData); + } + + public bool Equals(ContentData? other) + { + return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other)); + } + + public override int GetHashCode() + { + return this.DictionaryHashCode(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs new file mode 100644 index 000000000..d00c21552 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs @@ -0,0 +1,70 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public sealed class ContentFieldData : Dictionary, IEquatable + { + public ContentFieldData() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + public ContentFieldData AddValue(object? value) + { + return AddJsonValue(JsonValue.Create(value)); + } + + public ContentFieldData AddValue(string key, object? value) + { + return AddJsonValue(key, JsonValue.Create(value)); + } + + public ContentFieldData AddJsonValue(IJsonValue value) + { + this[InvariantPartitioning.Key] = value; + + return this; + } + + public ContentFieldData AddJsonValue(string key, IJsonValue value) + { + Guard.NotNullOrEmpty(key); + + if (Language.IsValidLanguage(key)) + { + this[key] = value; + } + else + { + this[key] = value; + } + + return this; + } + + public override bool Equals(object? obj) + { + return Equals(obj as ContentFieldData); + } + + public bool Equals(ContentFieldData? other) + { + return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other)); + } + + public override int GetHashCode() + { + return this.DictionaryHashCode(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs new file mode 100644 index 000000000..a4c6c4291 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public sealed class IdContentData : ContentData, IEquatable + { + public IdContentData() + : base(EqualityComparer.Default) + { + } + + public IdContentData(int capacity) + : base(capacity, EqualityComparer.Default) + { + } + + public static IdContentData Merge(params IdContentData[] contents) + { + return MergeTo(new IdContentData(), contents); + } + + public IdContentData MergeInto(IdContentData target) + { + return Merge(target, this); + } + + public IdContentData ToCleaned() + { + return Clean(this, new IdContentData()); + } + + public IdContentData AddField(long id, ContentFieldData? data) + { + Guard.GreaterThan(id, 0); + + this[id] = data; + + return this; + } + + public bool Equals(IdContentData other) + { + return base.Equals(other); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs new file mode 100644 index 000000000..2d31c07e6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Newtonsoft; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.Contents.Json +{ + public sealed class ContentFieldDataConverter : JsonClassConverter + { + protected override void WriteValue(JsonWriter writer, ContentFieldData value, JsonSerializer serializer) + { + writer.WriteStartObject(); + + foreach (var kvp in value) + { + writer.WritePropertyName(kvp.Key); + + serializer.Serialize(writer, kvp.Value); + } + + writer.WriteEndObject(); + } + + protected override ContentFieldData ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) + { + var result = new ContentFieldData(); + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + var propertyName = reader.Value.ToString()!; + + if (!reader.Read()) + { + throw new JsonSerializationException("Unexpected end when reading Object."); + } + + var value = serializer.Deserialize(reader); + + if (Language.IsValidLanguage(propertyName) || propertyName == InvariantPartitioning.Key) + { + propertyName = string.Intern(propertyName); + } + + result[propertyName] = value; + break; + case JsonToken.EndObject: + return result; + } + } + + throw new JsonSerializationException("Unexpected end when reading Object."); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs new file mode 100644 index 000000000..7f2ced411 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschrnkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Newtonsoft.Json; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Core.Contents.Json +{ + public class JsonWorkflowTransition + { + [JsonProperty] + public string Expression { get; set; } + + [JsonProperty] + public string Role { get; set; } + + [JsonProperty] + public List Roles { get; } + + public JsonWorkflowTransition() + { + } + + public JsonWorkflowTransition(WorkflowTransition client) + { + SimpleMapper.Map(client, this); + } + + public WorkflowTransition ToTransition() + { + var rolesList = Roles; + + if (!string.IsNullOrEmpty(Role)) + { + rolesList = new List { Role }; + } + + ReadOnlyCollection? roles = null; + + if (rolesList != null && rolesList.Count > 0) + { + roles = new ReadOnlyCollection(rolesList); + } + + return new WorkflowTransition(Expression, roles); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionConverter.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs new file mode 100644 index 000000000..aea3e31e7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public sealed class NamedContentData : ContentData, IEquatable + { + public NamedContentData() + : base(StringComparer.Ordinal) + { + } + + public NamedContentData(int capacity) + : base(capacity, StringComparer.Ordinal) + { + } + + public static NamedContentData Merge(params NamedContentData[] contents) + { + return MergeTo(new NamedContentData(), contents); + } + + public NamedContentData MergeInto(NamedContentData target) + { + return Merge(target, this); + } + + public NamedContentData ToCleaned() + { + return Clean(this, new NamedContentData()); + } + + public NamedContentData AddField(string name, ContentFieldData? data) + { + Guard.NotNullOrEmpty(name); + + this[name] = data; + + return this; + } + + public bool Equals(NamedContentData other) + { + return base.Equals(other); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs new file mode 100644 index 000000000..0f2f2ac8b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.ComponentModel; + +namespace Squidex.Domain.Apps.Core.Contents +{ + [TypeConverter(typeof(StatusConverter))] + public struct Status : IEquatable + { + public static readonly Status Archived = new Status("Archived"); + public static readonly Status Draft = new Status("Draft"); + public static readonly Status Published = new Status("Published"); + + private readonly string? name; + + public string Name + { + get { return name ?? "Unknown"; } + } + + public Status(string? name) + { + this.name = name; + } + + public override bool Equals(object? obj) + { + return obj is Status status && Equals(status); + } + + public bool Equals(Status other) + { + return string.Equals(name, other.name); + } + + public override int GetHashCode() + { + return name?.GetHashCode() ?? 0; + } + + public override string ToString() + { + return Name; + } + + public static bool operator ==(Status lhs, Status rhs) + { + return lhs.Equals(rhs); + } + + public static bool operator !=(Status lhs, Status rhs) + { + return !lhs.Equals(rhs); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs new file mode 100644 index 000000000..f580cfc27 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public sealed class StatusConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return destinationType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + return new Status(value?.ToString()); + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + return value.ToString()!; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs new file mode 100644 index 000000000..9bc70ab86 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs @@ -0,0 +1,126 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public sealed class Workflow : Named + { + private const string DefaultName = "Unnamed"; + + public static readonly IReadOnlyDictionary EmptySteps = new Dictionary(); + public static readonly IReadOnlyList EmptySchemaIds = new List(); + public static readonly Workflow Default = CreateDefault(); + public static readonly Workflow Empty = new Workflow(default, EmptySteps); + + public IReadOnlyDictionary Steps { get; } = EmptySteps; + + public IReadOnlyList SchemaIds { get; } = EmptySchemaIds; + + public Status Initial { get; } + + public Workflow( + Status initial, + IReadOnlyDictionary? steps, + IReadOnlyList? schemaIds = null, + string? name = null) + : base(name ?? DefaultName) + { + Initial = initial; + + if (steps != null) + { + Steps = steps; + } + + if (schemaIds != null) + { + SchemaIds = schemaIds; + } + } + + public static Workflow CreateDefault(string? name = null) + { + return new Workflow( + Status.Draft, new Dictionary + { + [Status.Archived] = + new WorkflowStep( + new Dictionary + { + [Status.Draft] = new WorkflowTransition() + }, + StatusColors.Archived, true), + [Status.Draft] = + new WorkflowStep( + new Dictionary + { + [Status.Archived] = new WorkflowTransition(), + [Status.Published] = new WorkflowTransition() + }, + StatusColors.Draft), + [Status.Published] = + new WorkflowStep( + new Dictionary + { + [Status.Archived] = new WorkflowTransition(), + [Status.Draft] = new WorkflowTransition() + }, + StatusColors.Published) + }, null, name); + } + + public IEnumerable<(Status Status, WorkflowStep Step, WorkflowTransition Transition)> GetTransitions(Status status) + { + if (TryGetStep(status, out var step)) + { + foreach (var transition in step.Transitions) + { + yield return (transition.Key, Steps[transition.Key], transition.Value); + } + } + else if (TryGetStep(Initial, out var initial)) + { + yield return (Initial, initial, WorkflowTransition.Default); + } + } + + public bool TryGetTransition(Status from, Status to, [MaybeNullWhen(false)] out WorkflowTransition transition) + { + transition = null!; + + if (TryGetStep(from, out var step)) + { + if (step.Transitions.TryGetValue(to, out transition!)) + { + return true; + } + } + else if (to == Initial) + { + transition = WorkflowTransition.Default; + + return true; + } + + return false; + } + + public bool TryGetStep(Status status, [MaybeNullWhen(false)] out WorkflowStep step) + { + return Steps.TryGetValue(status, out step!); + } + + public (Status Key, WorkflowStep) GetInitialStep() + { + return (Initial, Steps[Initial]); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs new file mode 100644 index 000000000..5e5d97217 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public sealed class WorkflowStep + { + private static readonly IReadOnlyDictionary EmptyTransitions = new Dictionary(); + + public IReadOnlyDictionary Transitions { get; } + + public string? Color { get; } + + public bool NoUpdate { get; } + + public WorkflowStep(IReadOnlyDictionary? transitions = null, string? color = null, bool noUpdate = false) + { + Transitions = transitions ?? EmptyTransitions; + + Color = color; + + NoUpdate = noUpdate; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs new file mode 100644 index 000000000..c5cbc4581 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.ObjectModel; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public sealed class WorkflowTransition + { + public static readonly WorkflowTransition Default = new WorkflowTransition(); + + public string? Expression { get; } + + public ReadOnlyCollection? Roles { get; } + + public WorkflowTransition(string? expression = null, ReadOnlyCollection? roles = null) + { + Expression = expression; + + Roles = roles; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs new file mode 100644 index 000000000..dd18859a3 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs @@ -0,0 +1,83 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public sealed class Workflows : ArrayDictionary + { + public static readonly Workflows Empty = new Workflows(); + + private Workflows() + { + } + + public Workflows(KeyValuePair[] items) + : base(items) + { + } + + [Pure] + public Workflows Remove(Guid id) + { + return new Workflows(Without(id)); + } + + [Pure] + public Workflows Add(Guid workflowId, string name) + { + Guard.NotNullOrEmpty(name); + + return new Workflows(With(workflowId, Workflow.CreateDefault(name))); + } + + [Pure] + public Workflows Set(Workflow workflow) + { + Guard.NotNull(workflow); + + return new Workflows(With(Guid.Empty, workflow)); + } + + [Pure] + public Workflows Set(Guid id, Workflow workflow) + { + Guard.NotNull(workflow); + + return new Workflows(With(id, workflow)); + } + + [Pure] + public Workflows Update(Guid id, Workflow workflow) + { + Guard.NotNull(workflow); + + if (id == Guid.Empty) + { + return Set(workflow); + } + + if (!ContainsKey(id)) + { + return this; + } + + return new Workflows(With(id, workflow)); + } + + public Workflow GetFirst() + { + return Values.FirstOrDefault() ?? Workflow.Default; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml b/backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml rename to backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml diff --git a/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd b/backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd rename to backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd diff --git a/src/Squidex.Domain.Apps.Core.Model/Freezable.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Freezable.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs b/backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs b/backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs b/backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs new file mode 100644 index 000000000..a15d54402 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Squidex.Domain.Apps.Core +{ + public sealed class InvariantPartitioning : IFieldPartitioning, IFieldPartitionItem + { + public static readonly InvariantPartitioning Instance = new InvariantPartitioning(); + public static readonly string Key = "iv"; + + public int Count + { + get { return 1; } + } + + public IFieldPartitionItem Master + { + get { return this; } + } + + string IFieldPartitionItem.Key + { + get { return Key; } + } + + string IFieldPartitionItem.Name + { + get { return "Invariant"; } + } + + bool IFieldPartitionItem.IsOptional + { + get { return false; } + } + + IEnumerable IFieldPartitionItem.Fallback + { + get { return Enumerable.Empty(); } + } + + private InvariantPartitioning() + { + } + + public bool TryGetItem(string key, [MaybeNullWhen(false)] out IFieldPartitionItem item) + { + var isFound = string.Equals(key, Key, StringComparison.OrdinalIgnoreCase); + + item = isFound ? this : null!; + + return isFound; + } + + IEnumerator IEnumerable.GetEnumerator() + { + yield return this; + } + + IEnumerator IEnumerable.GetEnumerator() + { + yield return this; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Named.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Named.cs new file mode 100644 index 000000000..66826f9a2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Named.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core +{ + public abstract class Named + { + public string Name { get; } + + protected Named(string name) + { + Guard.NotNullOrEmpty(name); + + Name = name; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs new file mode 100644 index 000000000..d37f73609 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core +{ + public delegate IFieldPartitioning PartitionResolver(Partitioning key); + + public sealed class Partitioning : IEquatable + { + public static readonly Partitioning Invariant = new Partitioning("invariant"); + public static readonly Partitioning Language = new Partitioning("language"); + + public string Key { get; } + + public Partitioning(string key) + { + Guard.NotNullOrEmpty(key); + + Key = key; + } + + public override bool Equals(object? obj) + { + return Equals(obj as Partitioning); + } + + public bool Equals(Partitioning? other) + { + return string.Equals(other?.Key, Key, StringComparison.OrdinalIgnoreCase); + } + + public override int GetHashCode() + { + return Key.GetHashCode(); + } + + public override string ToString() + { + return Key; + } + + public static Partitioning FromString(string? value) + { + var isLanguage = string.Equals(value, Language.Key, StringComparison.OrdinalIgnoreCase); + + return isLanguage ? Language : Invariant; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs new file mode 100644 index 000000000..0dc60efd7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Core +{ + public static class PartitioningExtensions + { + private static readonly HashSet AllowedPartitions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + Partitioning.Language.Key, + Partitioning.Invariant.Key + }; + + public static bool IsValidPartitioning(this string? value) + { + return value == null || AllowedPartitions.Contains(value); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs new file mode 100644 index 000000000..5d3911ab8 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Rules +{ + public sealed class Rule : Cloneable + { + private RuleTrigger trigger; + private RuleAction action; + private string name; + private bool isEnabled = true; + + public string Name + { + get { return name; } + } + + public RuleTrigger Trigger + { + get { return trigger; } + } + + public RuleAction Action + { + get { return action; } + } + + public bool IsEnabled + { + get { return isEnabled; } + } + + public Rule(RuleTrigger trigger, RuleAction action) + { + Guard.NotNull(trigger); + Guard.NotNull(action); + + this.trigger = trigger; + this.trigger.Freeze(); + + this.action = action; + this.action.Freeze(); + } + + [Pure] + public Rule Rename(string name) + { + return Clone(clone => + { + clone.name = name; + }); + } + + [Pure] + public Rule Enable() + { + return Clone(clone => + { + clone.isEnabled = true; + }); + } + + [Pure] + public Rule Disable() + { + return Clone(clone => + { + clone.isEnabled = false; + }); + } + + [Pure] + public Rule Update(RuleTrigger newTrigger) + { + Guard.NotNull(newTrigger); + + if (newTrigger.GetType() != trigger.GetType()) + { + throw new ArgumentException("New trigger has another type.", nameof(newTrigger)); + } + + newTrigger.Freeze(); + + return Clone(clone => + { + clone.trigger = newTrigger; + }); + } + + [Pure] + public Rule Update(RuleAction newAction) + { + Guard.NotNull(newAction); + + if (newAction.GetType() != action.GetType()) + { + throw new ArgumentException("New action has another type.", nameof(newAction)); + } + + newAction.Freeze(); + + return Clone(clone => + { + clone.action = newAction; + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs new file mode 100644 index 000000000..0d797b6f4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Core.Rules.Triggers +{ + public sealed class ContentChangedTriggerSchemaV2 : Freezable + { + public Guid SchemaId { get; set; } + + public string? Condition { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs new file mode 100644 index 000000000..dffa597bb --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs @@ -0,0 +1,91 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class ArrayField : RootField, IArrayField + { + private FieldCollection fields = FieldCollection.Empty; + + public IReadOnlyList Fields + { + get { return fields.Ordered; } + } + + public IReadOnlyDictionary FieldsById + { + get { return fields.ById; } + } + + public IReadOnlyDictionary FieldsByName + { + get { return fields.ByName; } + } + + public FieldCollection FieldCollection + { + get { return fields; } + } + + public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) + : base(id, name, partitioning, properties, settings) + { + } + + public ArrayField(long id, string name, Partitioning partitioning, NestedField[] fields, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) + : this(id, name, partitioning, properties, settings) + { + Guard.NotNull(fields); + + this.fields = new FieldCollection(fields); + } + + [Pure] + public ArrayField DeleteField(long fieldId) + { + return Updatefields(f => f.Remove(fieldId)); + } + + [Pure] + public ArrayField ReorderFields(List ids) + { + return Updatefields(f => f.Reorder(ids)); + } + + [Pure] + public ArrayField AddField(NestedField field) + { + return Updatefields(f => f.Add(field)); + } + + [Pure] + public ArrayField UpdateField(long fieldId, Func updater) + { + return Updatefields(f => f.Update(fieldId, updater)); + } + + private ArrayField Updatefields(Func, FieldCollection> updater) + { + var newFields = updater(fields); + + if (ReferenceEquals(newFields, fields)) + { + return this; + } + + return Clone(clone => + { + clone.fields = newFields; + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs new file mode 100644 index 000000000..c0feda7df --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class ArrayFieldProperties : FieldProperties + { + public int? MinItems { get; set; } + + public int? MaxItems { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IArrayField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Array(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + throw new NotSupportedException(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs new file mode 100644 index 000000000..542b72439 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.ObjectModel; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class AssetsFieldProperties : FieldProperties + { + public bool MustBeImage { get; set; } + + public int? MinItems { get; set; } + + public int? MaxItems { get; set; } + + public int? MinWidth { get; set; } + + public int? MaxWidth { get; set; } + + public int? MinHeight { get; set; } + + public int? MaxHeight { get; set; } + + public int? MinSize { get; set; } + + public int? MaxSize { get; set; } + + public int? AspectWidth { get; set; } + + public int? AspectHeight { get; set; } + + public bool AllowDuplicates { get; set; } + + public bool ResolveImage { get; set; } + + public ReadOnlyCollection? AllowedExtensions { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Assets(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Assets(id, name, this, settings); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs new file mode 100644 index 000000000..d89db1e8c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class BooleanFieldProperties : FieldProperties + { + public bool? DefaultValue { get; set; } + + public bool InlineEditable { get; set; } + + public BooleanFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Boolean(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Boolean(id, name, this, settings); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs new file mode 100644 index 000000000..f3615d121 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class DateTimeFieldProperties : FieldProperties + { + public Instant? MaxValue { get; set; } + + public Instant? MinValue { get; set; } + + public Instant? DefaultValue { get; set; } + + public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; } + + public DateTimeFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.DateTime(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.DateTime(id, name, this, settings); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs new file mode 100644 index 000000000..d19ddfef4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs @@ -0,0 +1,171 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using Squidex.Infrastructure; + +#pragma warning disable IDE0044 // Add readonly modifier + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class FieldCollection : Cloneable> where T : IField + { + public static readonly FieldCollection Empty = new FieldCollection(); + + private static readonly Dictionary EmptyById = new Dictionary(); + private static readonly Dictionary EmptyByString = new Dictionary(); + + private T[] fieldsOrdered; + private Dictionary? fieldsById; + private Dictionary? fieldsByName; + + public IReadOnlyList Ordered + { + get { return fieldsOrdered; } + } + + public IReadOnlyDictionary ById + { + get + { + if (fieldsById == null) + { + if (fieldsOrdered.Length == 0) + { + fieldsById = EmptyById; + } + else + { + fieldsById = fieldsOrdered.ToDictionary(x => x.Id); + } + } + + return fieldsById; + } + } + + public IReadOnlyDictionary ByName + { + get + { + if (fieldsByName == null) + { + if (fieldsOrdered.Length == 0) + { + fieldsByName = EmptyByString; + } + else + { + fieldsByName = fieldsOrdered.ToDictionary(x => x.Name); + } + } + + return fieldsByName; + } + } + + private FieldCollection() + { + fieldsOrdered = Array.Empty(); + } + + public FieldCollection(T[] fields) + { + Guard.NotNull(fields); + + fieldsOrdered = fields; + } + + protected override void OnCloned() + { + fieldsById = null; + fieldsByName = null; + } + + [Pure] + public FieldCollection Remove(long fieldId) + { + if (!ById.TryGetValue(fieldId, out _)) + { + return this; + } + + return Clone(clone => + { + clone.fieldsOrdered = fieldsOrdered.Where(x => x.Id != fieldId).ToArray(); + }); + } + + [Pure] + public FieldCollection Reorder(List ids) + { + Guard.NotNull(ids); + + if (ids.Count != fieldsOrdered.Length || ids.Any(x => !ById.ContainsKey(x))) + { + throw new ArgumentException("Ids must cover all fields.", nameof(ids)); + } + + return Clone(clone => + { + clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToArray(); + }); + } + + [Pure] + public FieldCollection Add(T field) + { + Guard.NotNull(field); + + if (ByName.ContainsKey(field.Name)) + { + throw new ArgumentException($"A field with name '{field.Name}' already exists.", nameof(field)); + } + + if (ById.ContainsKey(field.Id)) + { + throw new ArgumentException($"A field with id {field.Id} already exists.", nameof(field)); + } + + return Clone(clone => + { + clone.fieldsOrdered = clone.fieldsOrdered.Union(Enumerable.Repeat(field, 1)).ToArray(); + }); + } + + [Pure] + public FieldCollection Update(long fieldId, Func updater) + { + Guard.NotNull(updater); + + if (!ById.TryGetValue(fieldId, out var field)) + { + return this; + } + + var newField = updater(field); + + if (ReferenceEquals(newField, field)) + { + return this; + } + + if (!(newField is T)) + { + throw new InvalidOperationException($"Field must be of type {typeof(T)}"); + } + + return Clone(clone => + { + clone.fieldsOrdered = clone.fieldsOrdered.Select(x => ReferenceEquals(x, field) ? newField : x).ToArray(); + }); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs new file mode 100644 index 000000000..a0578f698 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.ObjectModel; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public abstract class FieldProperties : NamedElementPropertiesBase + { + public bool IsRequired { get; set; } + + public bool IsListField { get; set; } + + public bool IsReferenceField { get; set; } + + public string? Placeholder { get; set; } + + public string? EditorUrl { get; set; } + + public ReadOnlyCollection Tags { get; set; } + + public abstract T Accept(IFieldPropertiesVisitor visitor); + + public abstract T Accept(IFieldVisitor visitor, IField field); + + public abstract RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null); + + public abstract NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null); + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs new file mode 100644 index 000000000..9af19f67f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs @@ -0,0 +1,236 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public static class Fields + { + public static RootField Array(long id, string name, Partitioning partitioning, params NestedField[] fields) + { + return new ArrayField(id, name, partitioning, fields); + } + + public static ArrayField Array(long id, string name, Partitioning partitioning, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new ArrayField(id, name, partitioning, properties, settings); + } + + public static RootField Assets(long id, string name, Partitioning partitioning, AssetsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField Boolean(long id, string name, Partitioning partitioning, BooleanFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField DateTime(long id, string name, Partitioning partitioning, DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField Geolocation(long id, string name, Partitioning partitioning, GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField Json(long id, string name, Partitioning partitioning, JsonFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField Number(long id, string name, Partitioning partitioning, NumberFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField References(long id, string name, Partitioning partitioning, ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField String(long id, string name, Partitioning partitioning, StringFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField Tags(long id, string name, Partitioning partitioning, TagsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField UI(long id, string name, Partitioning partitioning, UIFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static NestedField Assets(long id, string name, AssetsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField Boolean(long id, string name, BooleanFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField DateTime(long id, string name, DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField Geolocation(long id, string name, GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField Json(long id, string name, JsonFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField Number(long id, string name, NumberFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField References(long id, string name, ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField String(long id, string name, StringFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField Tags(long id, string name, TagsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField UI(long id, string name, UIFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, Func? handler = null, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) + { + var field = Array(id, name, partitioning, properties, settings); + + if (handler != null) + { + field = handler(field); + } + + return schema.AddField(field); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs new file mode 100644 index 000000000..c28ce7b29 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class GeolocationFieldProperties : FieldProperties + { + public GeolocationFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Geolocation(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Geolocation(id, name, this, settings); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldSettings.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldSettings.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldSettings.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldSettings.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs new file mode 100644 index 000000000..06839a5d2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Squidex.Infrastructure; +using P = Squidex.Domain.Apps.Core.Partitioning; + +namespace Squidex.Domain.Apps.Core.Schemas.Json +{ + public sealed class JsonFieldModel : IFieldSettings + { + [JsonProperty] + public long Id { get; set; } + + [JsonProperty] + public string Name { get; set; } + + [JsonProperty] + public string Partitioning { get; set; } + + [JsonProperty] + public bool IsHidden { get; set; } + + [JsonProperty] + public bool IsLocked { get; set; } + + [JsonProperty] + public bool IsDisabled { get; set; } + + [JsonProperty] + public FieldProperties Properties { get; set; } + + [JsonProperty] + public JsonNestedFieldModel[]? Children { get; set; } + + public RootField ToField() + { + var partitioning = P.FromString(Partitioning); + + if (Properties is ArrayFieldProperties arrayProperties) + { + var nested = Children?.Map(n => n.ToNestedField()) ?? Array.Empty(); + + return new ArrayField(Id, Name, partitioning, nested, arrayProperties, this); + } + else + { + return Properties.CreateRootField(Id, Name, partitioning, this); + } + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs new file mode 100644 index 000000000..7f747da01 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs @@ -0,0 +1,111 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Core.Schemas.Json +{ + public sealed class JsonSchemaModel + { + [JsonProperty] + public string Name { get; set; } + + [JsonProperty] + public string Category { get; set; } + + [JsonProperty] + public bool IsSingleton { get; set; } + + [JsonProperty] + public bool IsPublished { get; set; } + + [JsonProperty] + public SchemaProperties Properties { get; set; } + + [JsonProperty] + public SchemaScripts Scripts { get; set; } + + [JsonProperty] + public JsonFieldModel[] Fields { get; set; } + + [JsonProperty] + public Dictionary PreviewUrls { get; set; } + + public JsonSchemaModel() + { + } + + public JsonSchemaModel(Schema schema) + { + SimpleMapper.Map(schema, this); + + Fields = + schema.Fields.Select(x => + new JsonFieldModel + { + Id = x.Id, + Name = x.Name, + Children = CreateChildren(x), + IsHidden = x.IsHidden, + IsLocked = x.IsLocked, + IsDisabled = x.IsDisabled, + Partitioning = x.Partitioning.Key, + Properties = x.RawProperties + }).ToArray(); + + PreviewUrls = schema.PreviewUrls.ToDictionary(x => x.Key, x => x.Value); + } + + private static JsonNestedFieldModel[]? CreateChildren(IField field) + { + if (field is ArrayField arrayField) + { + return arrayField.Fields.Select(x => + new JsonNestedFieldModel + { + Id = x.Id, + Name = x.Name, + IsHidden = x.IsHidden, + IsLocked = x.IsLocked, + IsDisabled = x.IsDisabled, + Properties = x.RawProperties + }).ToArray(); + } + + return null; + } + + public Schema ToSchema() + { + var fields = Fields.Map(f => f.ToField()) ?? Array.Empty(); + + var schema = new Schema(Name, fields, Properties, IsPublished, IsSingleton); + + if (!string.IsNullOrWhiteSpace(Category)) + { + schema = schema.ChangeCategory(Category); + } + + if (Scripts != null) + { + schema = schema.ConfigureScripts(Scripts); + } + + if (PreviewUrls?.Count > 0) + { + schema = schema.ConfigurePreviewUrls(PreviewUrls); + } + + return schema; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs new file mode 100644 index 000000000..ae850ec31 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class JsonFieldProperties : FieldProperties + { + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Json(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Json(id, name, this, settings); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs new file mode 100644 index 000000000..3fae17358 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public abstract class NamedElementPropertiesBase : Freezable + { + public string? Label { get; set; } + + public string? Hints { get; set; } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs new file mode 100644 index 000000000..6fcef66be --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public abstract class NestedField : Cloneable, INestedField + { + private readonly long fieldId; + private readonly string fieldName; + private bool isDisabled; + private bool isHidden; + private bool isLocked; + + public long Id + { + get { return fieldId; } + } + + public string Name + { + get { return fieldName; } + } + + public bool IsLocked + { + get { return isLocked; } + } + + public bool IsHidden + { + get { return isHidden; } + } + + public bool IsDisabled + { + get { return isDisabled; } + } + + public abstract FieldProperties RawProperties { get; } + + protected NestedField(long id, string name, IFieldSettings? settings = null) + { + Guard.NotNullOrEmpty(name); + Guard.GreaterThan(id, 0); + + fieldId = id; + fieldName = name; + + if (settings != null) + { + isLocked = settings.IsLocked; + isHidden = settings.IsHidden; + isDisabled = settings.IsDisabled; + } + } + + [Pure] + public NestedField Lock() + { + return Clone(clone => + { + clone.isLocked = true; + }); + } + + [Pure] + public NestedField Hide() + { + return Clone(clone => + { + clone.isHidden = true; + }); + } + + [Pure] + public NestedField Show() + { + return Clone(clone => + { + clone.isHidden = false; + }); + } + + [Pure] + public NestedField Disable() + { + return Clone(clone => + { + clone.isDisabled = true; + }); + } + + [Pure] + public NestedField Enable() + { + return Clone(clone => + { + clone.isDisabled = false; + }); + } + + public abstract T Accept(IFieldVisitor visitor); + + public abstract NestedField Update(FieldProperties newProperties); + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs new file mode 100644 index 000000000..61ba3a6a8 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public class NestedField : NestedField, IField where T : FieldProperties, new() + { + private T properties; + + public T Properties + { + get { return properties; } + } + + public override FieldProperties RawProperties + { + get { return properties; } + } + + public NestedField(long id, string name, T? properties = null, IFieldSettings? settings = null) + : base(id, name, settings) + { + SetProperties(properties ?? new T()); + } + + [Pure] + public override NestedField Update(FieldProperties newProperties) + { + var typedProperties = ValidateProperties(newProperties); + + return Clone>(clone => + { + clone.SetProperties(typedProperties); + }); + } + + private void SetProperties(T newProperties) + { + properties = newProperties; + properties.Freeze(); + } + + private T ValidateProperties(FieldProperties newProperties) + { + Guard.NotNull(newProperties); + + if (!(newProperties is T typedProperties)) + { + throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties)); + } + + return typedProperties; + } + + public override TResult Accept(IFieldVisitor visitor) + { + return properties.Accept(visitor, this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs new file mode 100644 index 000000000..f38cbe0c3 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.ObjectModel; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class NumberFieldProperties : FieldProperties + { + public ReadOnlyCollection? AllowedValues { get; set; } + + public double? MaxValue { get; set; } + + public double? MinValue { get; set; } + + public double? DefaultValue { get; set; } + + public bool IsUnique { get; set; } + + public bool InlineEditable { get; set; } + + public NumberFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Number(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Number(id, name, this, settings); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs new file mode 100644 index 000000000..31131e4dc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class ReferencesFieldProperties : FieldProperties + { + public int? MinItems { get; set; } + + public int? MaxItems { get; set; } + + public bool ResolveReference { get; set; } + + public bool AllowDuplicates { get; set; } + + public ReferencesFieldEditor Editor { get; set; } + + public ReadOnlyCollection? SchemaIds { get; set; } + + public Guid SchemaId + { + set + { + if (value != default) + { + SchemaIds = new ReadOnlyCollection(new List { value }); + } + else + { + SchemaIds = null; + } + } + } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.References(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.References(id, name, this, settings); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs new file mode 100644 index 000000000..af0f94d07 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs @@ -0,0 +1,122 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public abstract class RootField : Cloneable, IRootField + { + private readonly long fieldId; + private readonly string fieldName; + private readonly Partitioning partitioning; + private bool isDisabled; + private bool isHidden; + private bool isLocked; + + public long Id + { + get { return fieldId; } + } + + public string Name + { + get { return fieldName; } + } + + public bool IsLocked + { + get { return isLocked; } + } + + public bool IsHidden + { + get { return isHidden; } + } + + public bool IsDisabled + { + get { return isDisabled; } + } + + public Partitioning Partitioning + { + get { return partitioning; } + } + + public abstract FieldProperties RawProperties { get; } + + protected RootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + Guard.NotNullOrEmpty(name); + Guard.GreaterThan(id, 0); + Guard.NotNull(partitioning); + + fieldId = id; + fieldName = name; + + this.partitioning = partitioning; + + if (settings != null) + { + isLocked = settings.IsLocked; + isHidden = settings.IsHidden; + isDisabled = settings.IsDisabled; + } + } + + [Pure] + public RootField Lock() + { + return Clone(clone => + { + clone.isLocked = true; + }); + } + + [Pure] + public RootField Hide() + { + return Clone(clone => + { + clone.isHidden = true; + }); + } + + [Pure] + public RootField Show() + { + return Clone(clone => + { + clone.isHidden = false; + }); + } + + [Pure] + public RootField Disable() + { + return Clone(clone => + { + clone.isDisabled = true; + }); + } + + [Pure] + public RootField Enable() + { + return Clone(clone => + { + clone.isDisabled = false; + }); + } + + public abstract T Accept(IFieldVisitor visitor); + + public abstract RootField Update(FieldProperties newProperties); + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs new file mode 100644 index 000000000..fffc1dc0b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public class RootField : RootField, IField where T : FieldProperties, new() + { + private T properties; + + public T Properties + { + get { return properties; } + } + + public override FieldProperties RawProperties + { + get { return properties; } + } + + public RootField(long id, string name, Partitioning partitioning, T? properties = null, IFieldSettings? settings = null) + : base(id, name, partitioning, settings) + { + SetProperties(properties ?? new T()); + } + + [Pure] + public override RootField Update(FieldProperties newProperties) + { + var typedProperties = ValidateProperties(newProperties); + + return Clone>(clone => + { + clone.SetProperties(typedProperties); + }); + } + + private void SetProperties(T newProperties) + { + properties = newProperties; + properties.Freeze(); + } + + private T ValidateProperties(FieldProperties newProperties) + { + Guard.NotNull(newProperties); + + if (!(newProperties is T typedProperties)) + { + throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties)); + } + + return typedProperties; + } + + public override TResult Accept(IFieldVisitor visitor) + { + return properties.Accept(visitor, this); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs new file mode 100644 index 000000000..2777eb65d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs @@ -0,0 +1,201 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class Schema : Cloneable + { + private static readonly Dictionary EmptyPreviewUrls = new Dictionary(); + private readonly string name; + private readonly bool isSingleton; + private string category; + private FieldCollection fields = FieldCollection.Empty; + private IReadOnlyDictionary previewUrls = EmptyPreviewUrls; + private SchemaScripts scripts = new SchemaScripts(); + private SchemaProperties properties; + private bool isPublished; + + public string Name + { + get { return name; } + } + + public string Category + { + get { return category; } + } + + public bool IsPublished + { + get { return isPublished; } + } + + public bool IsSingleton + { + get { return isSingleton; } + } + + public IReadOnlyList Fields + { + get { return fields.Ordered; } + } + + public IReadOnlyDictionary FieldsById + { + get { return fields.ById; } + } + + public IReadOnlyDictionary FieldsByName + { + get { return fields.ByName; } + } + + public IReadOnlyDictionary PreviewUrls + { + get { return previewUrls; } + } + + public FieldCollection FieldCollection + { + get { return fields; } + } + + public SchemaScripts Scripts + { + get { return scripts; } + } + + public SchemaProperties Properties + { + get { return properties; } + } + + public Schema(string name, SchemaProperties? properties = null, bool isSingleton = false) + { + Guard.NotNullOrEmpty(name); + + this.name = name; + + this.properties = properties ?? new SchemaProperties(); + this.properties.Freeze(); + + this.isSingleton = isSingleton; + } + + public Schema(string name, RootField[] fields, SchemaProperties properties, bool isPublished, bool isSingleton = false) + : this(name, properties, isSingleton) + { + Guard.NotNull(fields); + + this.fields = new FieldCollection(fields); + + this.isPublished = isPublished; + } + + [Pure] + public Schema Update(SchemaProperties newProperties) + { + Guard.NotNull(newProperties); + + return Clone(clone => + { + clone.properties = newProperties; + clone.properties.Freeze(); + }); + } + + [Pure] + public Schema ConfigureScripts(SchemaScripts newScripts) + { + return Clone(clone => + { + clone.scripts = newScripts ?? new SchemaScripts(); + clone.scripts.Freeze(); + }); + } + + [Pure] + public Schema Publish() + { + return Clone(clone => + { + clone.isPublished = true; + }); + } + + [Pure] + public Schema Unpublish() + { + return Clone(clone => + { + clone.isPublished = false; + }); + } + + [Pure] + public Schema ChangeCategory(string newCategory) + { + return Clone(clone => + { + clone.category = newCategory; + }); + } + + [Pure] + public Schema ConfigurePreviewUrls(IReadOnlyDictionary newPreviewUrls) + { + return Clone(clone => + { + clone.previewUrls = newPreviewUrls ?? EmptyPreviewUrls; + }); + } + + [Pure] + public Schema DeleteField(long fieldId) + { + return UpdateFields(f => f.Remove(fieldId)); + } + + [Pure] + public Schema ReorderFields(List ids) + { + return UpdateFields(f => f.Reorder(ids)); + } + + [Pure] + public Schema AddField(RootField field) + { + return UpdateFields(f => f.Add(field)); + } + + [Pure] + public Schema UpdateField(long fieldId, Func updater) + { + return UpdateFields(f => f.Update(fieldId, updater)); + } + + private Schema UpdateFields(Func, FieldCollection> updater) + { + var newFields = updater(fields); + + if (ReferenceEquals(newFields, fields)) + { + return this; + } + + return Clone(clone => + { + clone.fields = newFields; + }); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs new file mode 100644 index 000000000..d58770d83 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs @@ -0,0 +1,52 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.ObjectModel; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class StringFieldProperties : FieldProperties + { + public ReadOnlyCollection? AllowedValues { get; set; } + + public int? MinLength { get; set; } + + public int? MaxLength { get; set; } + + public bool IsUnique { get; set; } + + public bool InlineEditable { get; set; } + + public string? DefaultValue { get; set; } + + public string? Pattern { get; set; } + + public string? PatternMessage { get; set; } + + public StringFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.String(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.String(id, name, this, settings); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldNormalization.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldNormalization.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldNormalization.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldNormalization.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs new file mode 100644 index 000000000..ffa0fc1c5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.ObjectModel; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class TagsFieldProperties : FieldProperties + { + public ReadOnlyCollection? AllowedValues { get; set; } + + public int? MinItems { get; set; } + + public int? MaxItems { get; set; } + + public TagsFieldEditor Editor { get; set; } + + public TagsFieldNormalization Normalization { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Tags(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Tags(id, name, this, settings); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs new file mode 100644 index 000000000..cd7741e8c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class UIFieldProperties : FieldProperties + { + public UIFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return new NestedField(id, name, this, settings); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, this, settings); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj new file mode 100644 index 000000000..3cad63271 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -0,0 +1,31 @@ + + + netcoreapp3.0 + Squidex.Domain.Apps.Core + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs b/backend/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs new file mode 100644 index 000000000..3ff9d90be --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs @@ -0,0 +1,160 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Text; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ConvertContent +{ + public static class ContentConverter + { + private static readonly Func KeyNameResolver = f => f.Name; + private static readonly Func KeyIdResolver = f => f.Id; + + public static string ToFullText(this ContentData data, int maxTotalLength = 1024 * 1024, int maxFieldLength = 1000, string separator = " ") where T : notnull + { + var stringBuilder = new StringBuilder(); + + foreach (var fieldValue in data.Values) + { + if (fieldValue != null) + { + foreach (var value in fieldValue.Values) + { + AppendText(value, stringBuilder, maxFieldLength, separator, false); + } + } + } + + var result = stringBuilder.ToString(); + + if (result.Length > maxTotalLength) + { + result = result.Substring(0, maxTotalLength); + } + + return result; + } + + private static void AppendText(IJsonValue value, StringBuilder stringBuilder, int maxFieldLength, string separator, bool allowObjects) + { + if (value.Type == JsonValueType.String) + { + var text = value.ToString(); + + if (text.Length <= maxFieldLength) + { + if (stringBuilder.Length > 0) + { + stringBuilder.Append(separator); + } + + stringBuilder.Append(text); + } + } + else if (value is JsonArray array) + { + foreach (var item in array) + { + AppendText(item, stringBuilder, maxFieldLength, separator, true); + } + } + else if (value is JsonObject obj && allowObjects) + { + foreach (var item in obj.Values) + { + AppendText(item, stringBuilder, maxFieldLength, separator, true); + } + } + } + + public static NamedContentData ConvertId2Name(this IdContentData content, Schema schema, params FieldConverter[] converters) + { + Guard.NotNull(schema); + + var result = new NamedContentData(content.Count); + + return ConvertInternal(content, result, schema.FieldsById, KeyNameResolver, converters); + } + + public static IdContentData ConvertId2Id(this IdContentData content, Schema schema, params FieldConverter[] converters) + { + Guard.NotNull(schema); + + var result = new IdContentData(content.Count); + + return ConvertInternal(content, result, schema.FieldsById, KeyIdResolver, converters); + } + + public static NamedContentData ConvertName2Name(this NamedContentData content, Schema schema, params FieldConverter[] converters) + { + Guard.NotNull(schema); + + var result = new NamedContentData(content.Count); + + return ConvertInternal(content, result, schema.FieldsByName, KeyNameResolver, converters); + } + + public static IdContentData ConvertName2Id(this NamedContentData content, Schema schema, params FieldConverter[] converters) + { + Guard.NotNull(schema); + + var result = new IdContentData(content.Count); + + return ConvertInternal(content, result, schema.FieldsByName, KeyIdResolver, converters); + } + + private static TDict2 ConvertInternal( + TDict1 source, + TDict2 target, + IReadOnlyDictionary fields, + Func targetKey, params FieldConverter[] converters) + where TDict1 : IDictionary + where TDict2 : IDictionary + where TKey1 : notnull + where TKey2 : notnull + { + foreach (var fieldKvp in source) + { + if (!fields.TryGetValue(fieldKvp.Key, out var field)) + { + continue; + } + + ContentFieldData? newValue = fieldKvp.Value; + + if (newValue != null) + { + if (converters != null) + { + foreach (var converter in converters) + { + newValue = converter(newValue, field); + + if (newValue == null) + { + break; + } + } + } + } + + if (newValue != null) + { + target.Add(targetKey(field), newValue); + } + } + + return target; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs new file mode 100644 index 000000000..d313a4d5c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ConvertContent +{ + public static class ContentConverterFlat + { + public static object ToFlatLanguageModel(this NamedContentData content, LanguagesConfig languagesConfig, IReadOnlyCollection? languagePreferences = null) + { + Guard.NotNull(languagesConfig); + + if (languagePreferences == null || languagePreferences.Count == 0) + { + return content; + } + + if (languagePreferences.Count == 1 && languagesConfig.TryGetConfig(languagePreferences.First(), out var languageConfig)) + { + languagePreferences = languagePreferences.Union(languageConfig.LanguageFallbacks).ToList(); + } + + var result = new Dictionary(); + + foreach (var fieldValue in content) + { + var fieldData = fieldValue.Value; + + if (fieldData != null) + { + foreach (var language in languagePreferences) + { + if (fieldData.TryGetValue(language, out var value) && value.Type != JsonValueType.Null) + { + result[fieldValue.Key] = value; + + break; + } + } + } + } + + return result; + } + + public static Dictionary ToFlatten(this NamedContentData content) + { + var result = new Dictionary(); + + foreach (var fieldValue in content) + { + var fieldData = fieldValue.Value; + + if (fieldData?.Count == 1) + { + result[fieldValue.Key] = fieldData.Values.First(); + } + else + { + result[fieldValue.Key] = fieldData; + } + } + + return result; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs new file mode 100644 index 000000000..b6d7bc1c8 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs @@ -0,0 +1,373 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +#pragma warning disable RECS0002 // Convert anonymous method to method group + +namespace Squidex.Domain.Apps.Core.ConvertContent +{ + public delegate ContentFieldData? FieldConverter(ContentFieldData data, IRootField field); + + public static class FieldConverters + { + private static readonly Func KeyNameResolver = f => f.Name; + private static readonly Func KeyIdResolver = f => f.Id.ToString(); + + private static readonly Func FieldByIdResolver = + (f, k) => long.TryParse(k, out var id) ? f.FieldsById.GetOrDefault(id) : null; + + private static readonly Func FieldByNameResolver = + (f, k) => f.FieldsByName.GetOrDefault(k); + + public static FieldConverter ExcludeHidden() + { + return (data, field) => !field.IsForApi() ? null : data; + } + + public static FieldConverter ExcludeChangedTypes() + { + return (data, field) => + { + foreach (var value in data.Values) + { + if (value.Type == JsonValueType.Null) + { + continue; + } + + try + { + JsonValueConverter.ConvertValue(field, value); + } + catch + { + return null; + } + } + + return data; + }; + } + + public static FieldConverter ResolveAssetUrls(IReadOnlyCollection? fields, IAssetUrlGenerator urlGenerator) + { + if (fields?.Any() != true) + { + return (data, field) => data; + } + + var isAll = fields.First() == "*"; + + return (data, field) => + { + if (field is IField && (isAll || fields.Contains(field.Name))) + { + foreach (var partition in data) + { + if (partition.Value is JsonArray array) + { + for (var i = 0; i < array.Count; i++) + { + var id = array[i].ToString(); + + array[i] = JsonValue.Create(urlGenerator.GenerateUrl(id)); + } + } + } + } + + return data; + }; + } + + public static FieldConverter ResolveInvariant(LanguagesConfig config) + { + var codeForInvariant = InvariantPartitioning.Key; + var codeForMasterLanguage = config.Master.Language.Iso2Code; + + return (data, field) => + { + if (field.Partitioning.Equals(Partitioning.Invariant)) + { + var result = new ContentFieldData(); + + if (data.TryGetValue(codeForInvariant, out var value)) + { + result[codeForInvariant] = value; + } + else if (data.TryGetValue(codeForMasterLanguage, out value)) + { + result[codeForInvariant] = value; + } + else if (data.Count > 0) + { + result[codeForInvariant] = data.Values.First(); + } + + return result; + } + + return data; + }; + } + + public static FieldConverter ResolveLanguages(LanguagesConfig config) + { + var codeForInvariant = InvariantPartitioning.Key; + + return (data, field) => + { + if (field.Partitioning.Equals(Partitioning.Language)) + { + var result = new ContentFieldData(); + + foreach (var languageConfig in config) + { + var languageCode = languageConfig.Key; + + if (data.TryGetValue(languageCode, out var value)) + { + result[languageCode] = value; + } + else if (languageConfig == config.Master && data.TryGetValue(codeForInvariant, out value)) + { + result[languageCode] = value; + } + } + + return result; + } + + return data; + }; + } + + public static FieldConverter ResolveFallbackLanguages(LanguagesConfig config) + { + var master = config.Master; + + return (data, field) => + { + if (field.Partitioning.Equals(Partitioning.Language)) + { + foreach (var languageConfig in config) + { + var languageCode = languageConfig.Key; + + if (!data.TryGetValue(languageCode, out var value)) + { + var dataFound = false; + + foreach (var fallback in languageConfig.Fallback) + { + if (data.TryGetValue(fallback, out value)) + { + data[languageCode] = value; + dataFound = true; + break; + } + } + + if (!dataFound && languageConfig != master) + { + if (data.TryGetValue(master.Language, out value)) + { + data[languageCode] = value; + } + } + } + } + } + + return data; + }; + } + + public static FieldConverter FilterLanguages(LanguagesConfig config, IEnumerable? languages) + { + if (languages?.Any() != true) + { + return (data, field) => data; + } + + var languageSet = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var language in languages) + { + if (config.Contains(language.Iso2Code)) + { + languageSet.Add(language.Iso2Code); + } + } + + if (languageSet.Count == 0) + { + languageSet.Add(config.Master.Language.Iso2Code); + } + + return (data, field) => + { + if (field.Partitioning.Equals(Partitioning.Language)) + { + var result = new ContentFieldData(); + + foreach (var languageCode in languageSet) + { + if (data.TryGetValue(languageCode, out var value)) + { + result[languageCode] = value; + } + } + + return result; + } + + return data; + }; + } + + public static FieldConverter ForNestedName2Name(params ValueConverter[] converters) + { + return ForNested(FieldByNameResolver, KeyNameResolver, converters); + } + + public static FieldConverter ForNestedName2Id(params ValueConverter[] converters) + { + return ForNested(FieldByNameResolver, KeyIdResolver, converters); + } + + public static FieldConverter ForNestedId2Name(params ValueConverter[] converters) + { + return ForNested(FieldByIdResolver, KeyNameResolver, converters); + } + + public static FieldConverter ForNestedId2Id(params ValueConverter[] converters) + { + return ForNested(FieldByIdResolver, KeyIdResolver, converters); + } + + private static FieldConverter ForNested( + Func fieldResolver, + Func keyResolver, + params ValueConverter[] converters) + { + return (data, field) => + { + if (field is IArrayField arrayField) + { + var result = new ContentFieldData(); + + foreach (var partition in data) + { + if (!(partition.Value is JsonArray array)) + { + continue; + } + + var newArray = JsonValue.Array(); + + foreach (var item in array.OfType()) + { + var newItem = JsonValue.Object(); + + foreach (var kvp in item) + { + var nestedField = fieldResolver(arrayField, kvp.Key); + + if (nestedField == null) + { + continue; + } + + var newValue = kvp.Value; + + var isUnset = false; + + if (converters != null) + { + foreach (var converter in converters) + { + newValue = converter(newValue, nestedField); + + if (ReferenceEquals(newValue, Value.Unset)) + { + isUnset = true; + break; + } + } + } + + if (!isUnset) + { + newItem.Add(keyResolver(nestedField), newValue); + } + } + + newArray.Add(newItem); + } + + result.Add(partition.Key, newArray); + } + + return result; + } + + return data; + }; + } + + public static FieldConverter ForValues(params ValueConverter[] converters) + { + return (data, field) => + { + if (!(field is IArrayField)) + { + var result = new ContentFieldData(); + + foreach (var partition in data) + { + var newValue = partition.Value; + + var isUnset = false; + + if (converters != null) + { + foreach (var converter in converters) + { + newValue = converter(newValue, field); + + if (ReferenceEquals(newValue, Value.Unset)) + { + isUnset = true; + break; + } + } + } + + if (!isUnset) + { + result.Add(partition.Key, newValue); + } + } + + return result; + } + + return data; + }; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IAssetUrlGenerator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IAssetUrlGenerator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IAssetUrlGenerator.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IAssetUrlGenerator.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs new file mode 100644 index 000000000..53d4f7472 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.EnrichContent +{ + public sealed class ContentEnricher + { + private readonly Schema schema; + private readonly PartitionResolver partitionResolver; + + public ContentEnricher(Schema schema, PartitionResolver partitionResolver) + { + Guard.NotNull(schema); + Guard.NotNull(partitionResolver); + + this.schema = schema; + + this.partitionResolver = partitionResolver; + } + + public void Enrich(NamedContentData data) + { + Guard.NotNull(data); + + foreach (var field in schema.Fields) + { + var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData()); + + if (fieldData != null) + { + var fieldPartition = partitionResolver(field.Partitioning); + + foreach (var partitionItem in fieldPartition) + { + Enrich(field, fieldData, partitionItem); + } + + if (fieldData.Count > 0) + { + data[field.Name] = fieldData; + } + } + } + } + + private static void Enrich(IField field, ContentFieldData fieldData, IFieldPartitionItem partitionItem) + { + Guard.NotNull(fieldData); + + var defaultValue = DefaultValueFactory.CreateDefaultValue(field, SystemClock.Instance.GetCurrentInstant()); + + if (field.RawProperties.IsRequired || defaultValue == null || defaultValue.Type == JsonValueType.Null) + { + return; + } + + var key = partitionItem.Key; + + if (!fieldData.TryGetValue(key, out var value) || ShouldApplyDefaultValue(field, value)) + { + fieldData.AddJsonValue(key, defaultValue); + } + } + + private static bool ShouldApplyDefaultValue(IField field, IJsonValue value) + { + return value.Type == JsonValueType.Null || (field is IField && value is JsonScalar s && string.IsNullOrEmpty(s.Value)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs new file mode 100644 index 000000000..2f131b904 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs @@ -0,0 +1,97 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Globalization; +using NodaTime; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.EnrichContent +{ + public sealed class DefaultValueFactory : IFieldVisitor + { + private readonly Instant now; + + private DefaultValueFactory(Instant now) + { + this.now = now; + } + + public static IJsonValue CreateDefaultValue(IField field, Instant now) + { + Guard.NotNull(field); + + return field.Accept(new DefaultValueFactory(now)); + } + + public IJsonValue Visit(IArrayField field) + { + return JsonValue.Array(); + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Array(); + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Create(field.Properties.DefaultValue); + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Null; + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Null; + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Create(field.Properties.DefaultValue); + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Array(); + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Create(field.Properties.DefaultValue); + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Array(); + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Null; + } + + public IJsonValue Visit(IField field) + { + if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now) + { + return JsonValue.Create(now.ToString()); + } + + if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today) + { + return JsonValue.Create($"{now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}T00:00:00Z"); + } + + return JsonValue.Create(field.Properties.DefaultValue?.ToString()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs new file mode 100644 index 000000000..fa7b81d77 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs @@ -0,0 +1,224 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Core.EventSynchronization +{ + public static class SchemaSynchronizer + { + public static IEnumerable Synchronize(this Schema source, Schema? target, IJsonSerializer serializer, Func idGenerator, + SchemaSynchronizationOptions? options = null) + { + Guard.NotNull(source); + Guard.NotNull(serializer); + Guard.NotNull(idGenerator); + + if (target == null) + { + yield return new SchemaDeleted(); + } + else + { + options ??= new SchemaSynchronizationOptions(); + + static SchemaEvent E(SchemaEvent @event) + { + return @event; + } + + if (!source.Properties.EqualsJson(target.Properties, serializer)) + { + yield return E(new SchemaUpdated { Properties = target.Properties }); + } + + if (!source.Category.StringEquals(target.Category)) + { + yield return E(new SchemaCategoryChanged { Name = target.Category }); + } + + if (!source.Scripts.EqualsJson(target.Scripts, serializer)) + { + yield return E(new SchemaScriptsConfigured { Scripts = target.Scripts }); + } + + if (!source.PreviewUrls.EqualsDictionary(target.PreviewUrls)) + { + yield return E(new SchemaPreviewUrlsConfigured { PreviewUrls = target.PreviewUrls.ToDictionary(x => x.Key, x => x.Value) }); + } + + if (source.IsPublished != target.IsPublished) + { + yield return target.IsPublished ? + E(new SchemaPublished()) : + E(new SchemaUnpublished()); + } + + var events = SyncFields(source.FieldCollection, target.FieldCollection, serializer, idGenerator, CanUpdateRoot, null, options); + + foreach (var @event in events) + { + yield return E(@event); + } + } + } + + private static IEnumerable SyncFields( + FieldCollection source, + FieldCollection target, + IJsonSerializer serializer, + Func idGenerator, + Func canUpdate, + NamedId? parentId, SchemaSynchronizationOptions options) where T : class, IField + { + FieldEvent E(FieldEvent @event) + { + @event.ParentFieldId = parentId; + + return @event; + } + + var sourceIds = new List>(source.Ordered.Select(x => x.NamedId())); + var sourceNames = sourceIds.Select(x => x.Name).ToList(); + + if (!options.NoFieldDeletion) + { + foreach (var sourceField in source.Ordered) + { + if (!target.ByName.TryGetValue(sourceField.Name, out _)) + { + var id = sourceField.NamedId(); + + sourceIds.Remove(id); + sourceNames.Remove(id.Name); + + yield return E(new FieldDeleted { FieldId = id }); + } + } + } + + foreach (var targetField in target.Ordered) + { + NamedId? id = null; + + var canCreateField = true; + + if (source.ByName.TryGetValue(targetField.Name, out var sourceField)) + { + canCreateField = false; + + id = sourceField.NamedId(); + + if (canUpdate(sourceField, targetField)) + { + if (!sourceField.RawProperties.EqualsJson(targetField.RawProperties, serializer)) + { + yield return E(new FieldUpdated { FieldId = id, Properties = targetField.RawProperties }); + } + } + else if (!sourceField.IsLocked && !options.NoFieldRecreation) + { + canCreateField = true; + + sourceIds.Remove(id); + sourceNames.Remove(id.Name); + + yield return E(new FieldDeleted { FieldId = id }); + } + } + + if (canCreateField) + { + var partitioning = (string?)null; + + if (targetField is IRootField rootField) + { + partitioning = rootField.Partitioning.Key; + } + + id = NamedId.Of(idGenerator(), targetField.Name); + + yield return new FieldAdded + { + Name = targetField.Name, + ParentFieldId = parentId, + Partitioning = partitioning, + Properties = targetField.RawProperties, + FieldId = id + }; + + sourceIds.Add(id); + sourceNames.Add(id.Name); + } + + if (id != null && (sourceField == null || CanUpdate(sourceField, targetField))) + { + if (!targetField.IsLocked.BoolEquals(sourceField?.IsLocked)) + { + yield return E(new FieldLocked { FieldId = id }); + } + + if (!targetField.IsHidden.BoolEquals(sourceField?.IsHidden)) + { + yield return targetField.IsHidden ? + E(new FieldHidden { FieldId = id }) : + E(new FieldShown { FieldId = id }); + } + + if (!targetField.IsDisabled.BoolEquals(sourceField?.IsDisabled)) + { + yield return targetField.IsDisabled ? + E(new FieldDisabled { FieldId = id }) : + E(new FieldEnabled { FieldId = id }); + } + + if ((sourceField == null || sourceField is IArrayField) && targetField is IArrayField targetArrayField) + { + var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection.Empty; + + var events = SyncFields(fields, targetArrayField.FieldCollection, serializer, idGenerator, CanUpdate, id, options); + + foreach (var @event in events) + { + yield return @event; + } + } + } + } + + if (sourceNames.Count > 1) + { + var targetNames = target.Ordered.Select(x => x.Name); + + if (sourceNames.Intersect(targetNames).Count() == target.Ordered.Count && !sourceNames.SequenceEqual(targetNames)) + { + var fieldIds = targetNames.Select(x => sourceIds.FirstOrDefault(y => y.Name == x).Id).ToList(); + + yield return new SchemaFieldsReordered { FieldIds = fieldIds, ParentFieldId = parentId }; + } + } + } + + private static bool CanUpdateRoot(IRootField source, IRootField target) + { + return CanUpdate(source, target) && source.Partitioning == target.Partitioning; + } + + private static bool CanUpdate(IField source, IField target) + { + return !source.IsLocked && source.Name == target.Name && source.RawProperties.TypeEquals(target.RawProperties); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs new file mode 100644 index 000000000..080340380 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs @@ -0,0 +1,150 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +{ + public static class ContentReferencesExtensions + { + public static IEnumerable GetReferencedIds(this IdContentData source, Schema schema, Ids strategy = Ids.All) + { + Guard.NotNull(schema); + + foreach (var field in schema.Fields) + { + var ids = source.GetReferencedIds(field, strategy); + + foreach (var id in ids) + { + yield return id; + } + } + } + + public static IEnumerable GetReferencedIds(this IdContentData source, IField field, Ids strategy = Ids.All) + { + Guard.NotNull(field); + + if (source.TryGetValue(field.Id, out var fieldData) && fieldData != null) + { + foreach (var partitionValue in fieldData) + { + var ids = field.GetReferencedIds(partitionValue.Value, strategy); + + foreach (var id in ids) + { + yield return id; + } + } + } + } + + public static IEnumerable GetReferencedIds(this NamedContentData source, Schema schema, Ids strategy = Ids.All) + { + Guard.NotNull(schema); + + return GetReferencedIds(source, schema.Fields, strategy); + } + + public static IEnumerable GetReferencedIds(this NamedContentData source, IEnumerable fields, Ids strategy = Ids.All) + { + Guard.NotNull(fields); + + foreach (var field in fields) + { + var ids = source.GetReferencedIds(field, strategy); + + foreach (var id in ids) + { + yield return id; + } + } + } + + public static IEnumerable GetReferencedIds(this NamedContentData source, IField field, Ids strategy = Ids.All) + { + Guard.NotNull(field); + + if (source.TryGetValue(field.Name, out var fieldData) && fieldData != null) + { + foreach (var partitionValue in fieldData) + { + var ids = field.GetReferencedIds(partitionValue.Value, strategy); + + foreach (var id in ids) + { + yield return id; + } + } + } + } + + public static JsonObject FormatReferences(this NamedContentData data, Schema schema, LanguagesConfig languages, string separator = ", ") + { + Guard.NotNull(schema); + + var result = JsonValue.Object(); + + foreach (var language in languages) + { + result[language.Key] = JsonValue.Create(data.FormatReferenceFields(schema, language.Key, separator)); + } + + return result; + } + + private static string FormatReferenceFields(this NamedContentData data, Schema schema, string partition, string separator) + { + Guard.NotNull(schema); + + var sb = new StringBuilder(); + + void AddValue(object value) + { + if (sb.Length > 0) + { + sb.Append(separator); + } + + sb.Append(value); + } + + var referenceFields = schema.Fields.Where(x => x.RawProperties.IsReferenceField); + + if (!referenceFields.Any()) + { + referenceFields = schema.Fields.Take(1); + } + + foreach (var referenceField in referenceFields) + { + if (data.TryGetValue(referenceField.Name, out var fieldData) && fieldData != null) + { + if (fieldData.TryGetValue(partition, out var value)) + { + AddValue(value); + } + else if (fieldData.TryGetValue(InvariantPartitioning.Key, out var value2)) + { + AddValue(value2); + } + } + } + + return sb.ToString(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/Ids.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/Ids.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/Ids.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/Ids.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs new file mode 100644 index 000000000..c9bef6381 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs @@ -0,0 +1,109 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +{ + public sealed class ReferencesCleaner : IFieldVisitor + { + private readonly IJsonValue value; + private readonly ICollection? oldReferences; + + private ReferencesCleaner(IJsonValue value, ICollection? oldReferences) + { + this.value = value; + + this.oldReferences = oldReferences; + } + + public static IJsonValue CleanReferences(IField field, IJsonValue value, ICollection? oldReferences) + { + return field.Accept(new ReferencesCleaner(value, oldReferences)); + } + + public IJsonValue Visit(IField field) + { + return CleanIds(); + } + + public IJsonValue Visit(IField field) + { + if (oldReferences?.Contains(field.Properties.SingleId()) == true) + { + return JsonValue.Array(); + } + + return CleanIds(); + } + + private IJsonValue CleanIds() + { + var ids = value.ToGuidSet(); + + var isRemoved = false; + + if (oldReferences != null) + { + foreach (var oldReference in oldReferences) + { + isRemoved |= ids.Remove(oldReference); + } + } + + return isRemoved ? ids.ToJsonArray() : value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IArrayField field) + { + return value; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs new file mode 100644 index 000000000..bc13e06d8 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +{ + public static class ReferencesExtensions + { + public static IEnumerable GetReferencedIds(this IField field, IJsonValue? value, Ids strategy = Ids.All) + { + return ReferencesExtractor.ExtractReferences(field, value, strategy); + } + + public static IJsonValue CleanReferences(this IField field, IJsonValue value, ICollection? oldReferences) + { + if (IsNull(value)) + { + return value; + } + + return ReferencesCleaner.CleanReferences(field, value, oldReferences); + } + + private static bool IsNull(IJsonValue value) + { + return value == null || value.Type == JsonValueType.Null; + } + + public static JsonArray ToJsonArray(this HashSet ids) + { + var result = JsonValue.Array(); + + foreach (var id in ids) + { + result.Add(JsonValue.Create(id.ToString())); + } + + return result; + } + + public static HashSet ToGuidSet(this IJsonValue? value) + { + if (value is JsonArray array) + { + var result = new HashSet(); + + foreach (var id in array) + { + if (id.Type == JsonValueType.String && Guid.TryParse(id.ToString(), out var guid)) + { + result.Add(guid); + } + } + + return result; + } + + return new HashSet(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs new file mode 100644 index 000000000..dc11ff502 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +{ + public sealed class ReferencesExtractor : IFieldVisitor> + { + private readonly IJsonValue? value; + private readonly Ids strategy; + + private ReferencesExtractor(IJsonValue? value, Ids strategy) + { + this.value = value; + + this.strategy = strategy; + } + + public static IEnumerable ExtractReferences(IField field, IJsonValue? value, Ids strategy) + { + return field.Accept(new ReferencesExtractor(value, strategy)); + } + + public IEnumerable Visit(IArrayField field) + { + var result = new List(); + + if (value is JsonArray array) + { + foreach (var item in array.OfType()) + { + foreach (var nestedField in field.Fields) + { + if (item.TryGetValue(nestedField.Name, out var nestedValue)) + { + result.AddRange(nestedField.Accept(new ReferencesExtractor(nestedValue, strategy))); + } + } + } + } + + return result; + } + + public IEnumerable Visit(IField field) + { + var ids = value.ToGuidSet(); + + return ids; + } + + public IEnumerable Visit(IField field) + { + var ids = value.ToGuidSet(); + + if (strategy == Ids.All && field.Properties.SchemaIds != null) + { + foreach (var schemaId in field.Properties.SchemaIds) + { + ids.Add(schemaId); + } + } + + return ids; + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs new file mode 100644 index 000000000..83c35f680 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.OData.Edm; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.GenerateEdmSchema +{ + public delegate (EdmComplexType Type, bool Created) EdmTypeFactory(string names); + + public static class EdmSchemaExtensions + { + public static string EscapeEdmField(this string field) + { + return field.Replace("-", "_"); + } + + public static string UnescapeEdmField(this string field) + { + return field.Replace("_", "-"); + } + + public static EdmComplexType BuildEdmType(this Schema schema, bool withHidden, PartitionResolver partitionResolver, EdmTypeFactory typeFactory) + { + Guard.NotNull(typeFactory); + Guard.NotNull(partitionResolver); + + var (edmType, _) = typeFactory("Data"); + + var visitor = new EdmTypeVisitor(typeFactory); + + foreach (var field in schema.FieldsByName.Values) + { + if (!field.IsForApi(withHidden)) + { + continue; + } + + var fieldEdmType = field.Accept(visitor); + + if (fieldEdmType == null) + { + continue; + } + + var (partitionType, created) = typeFactory($"Data.{field.Name.ToPascalCase()}"); + + if (created) + { + var partition = partitionResolver(field.Partitioning); + + foreach (var partitionItem in partition) + { + partitionType.AddStructuralProperty(partitionItem.Key.EscapeEdmField(), fieldEdmType); + } + } + + edmType.AddStructuralProperty(field.Name.EscapeEdmField(), new EdmComplexTypeReference(partitionType, false)); + } + + return edmType; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs new file mode 100644 index 000000000..0fda2ba53 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs @@ -0,0 +1,103 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.OData.Edm; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.GenerateEdmSchema +{ + public sealed class EdmTypeVisitor : IFieldVisitor + { + private readonly EdmTypeFactory typeFactory; + + internal EdmTypeVisitor(EdmTypeFactory typeFactory) + { + this.typeFactory = typeFactory; + } + + public IEdmTypeReference? CreateEdmType(IField field) + { + return field.Accept(this); + } + + public IEdmTypeReference? Visit(IArrayField field) + { + var (fieldEdmType, created) = typeFactory($"Data.{field.Name.ToPascalCase()}.Item"); + + if (created) + { + foreach (var nestedField in field.Fields) + { + var nestedEdmType = nestedField.Accept(this); + + if (nestedEdmType != null) + { + fieldEdmType.AddStructuralProperty(nestedField.Name.EscapeEdmField(), nestedEdmType); + } + } + } + + return new EdmComplexTypeReference(fieldEdmType, false); + } + + public IEdmTypeReference? Visit(IField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.String, field); + } + + public IEdmTypeReference? Visit(IField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.Boolean, field); + } + + public IEdmTypeReference? Visit(IField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.DateTimeOffset, field); + } + + public IEdmTypeReference? Visit(IField field) + { + return null; + } + + public IEdmTypeReference? Visit(IField field) + { + return null; + } + + public IEdmTypeReference? Visit(IField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.Double, field); + } + + public IEdmTypeReference? Visit(IField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.String, field); + } + + public IEdmTypeReference? Visit(IField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.String, field); + } + + public IEdmTypeReference? Visit(IField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.String, field); + } + + public IEdmTypeReference? Visit(IField field) + { + return null; + } + + private static IEdmTypeReference CreatePrimitive(EdmPrimitiveTypeKind kind, IField field) + { + return EdmCoreModel.Instance.GetPrimitive(kind, !field.RawProperties.IsRequired); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs new file mode 100644 index 000000000..09e5aa07c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NJsonSchema; + +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +{ + public static class Builder + { + public static JsonSchema Object() + { + return new JsonSchema { Type = JsonObjectType.Object }; + } + + public static JsonSchema Guid() + { + return new JsonSchema { Type = JsonObjectType.String, Format = JsonFormatStrings.Guid }; + } + + public static JsonSchema String() + { + return new JsonSchema { Type = JsonObjectType.String }; + } + + public static JsonSchemaProperty ArrayProperty(JsonSchema item) + { + return new JsonSchemaProperty { Type = JsonObjectType.Array, Item = item }; + } + + public static JsonSchemaProperty BooleanProperty() + { + return new JsonSchemaProperty { Type = JsonObjectType.Boolean }; + } + + public static JsonSchemaProperty DateTimeProperty(string? description = null, bool isRequired = false) + { + return new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime, Description = description, IsRequired = isRequired }; + } + + public static JsonSchemaProperty GuidProperty(string? description = null, bool isRequired = false) + { + return new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.Guid, Description = description, IsRequired = isRequired }; + } + + public static JsonSchemaProperty NumberProperty(string? description = null, bool isRequired = false) + { + return new JsonSchemaProperty { Type = JsonObjectType.Number, Description = description, IsRequired = isRequired }; + } + + public static JsonSchemaProperty ObjectProperty(JsonSchema item, string? description = null, bool isRequired = false) + { + return new JsonSchemaProperty { Type = JsonObjectType.Object, Reference = item, Description = description, IsRequired = isRequired }; + } + + public static JsonSchemaProperty StringProperty(string? description = null, bool isRequired = false) + { + return new JsonSchemaProperty { Type = JsonObjectType.String, Description = description, IsRequired = isRequired }; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs new file mode 100644 index 000000000..306c33fc5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NJsonSchema; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +{ + public sealed class ContentSchemaBuilder + { + public JsonSchema CreateContentSchema(Schema schema, JsonSchema dataSchema) + { + Guard.NotNull(schema); + Guard.NotNull(dataSchema); + + var schemaName = schema.Properties.Label.WithFallback(schema.Name); + + var contentSchema = new JsonSchema + { + Properties = + { + ["id"] = Builder.GuidProperty($"The id of the {schemaName} content.", true), + ["data"] = Builder.ObjectProperty(dataSchema, $"The data of the {schemaName}.", true), + ["dataDraft"] = Builder.ObjectProperty(dataSchema, $"The draft data of the {schemaName}.", false), + ["version"] = Builder.NumberProperty($"The version of the {schemaName}.", true), + ["created"] = Builder.DateTimeProperty($"The date and time when the {schemaName} content has been created.", true), + ["createdBy"] = Builder.StringProperty($"The user that has created the {schemaName} content.", true), + ["lastModified"] = Builder.DateTimeProperty($"The date and time when the {schemaName} content has been modified last.", true), + ["lastModifiedBy"] = Builder.StringProperty($"The user that has updated the {schemaName} content last.", true), + ["status"] = Builder.StringProperty($"The status of the content.", true) + }, + Type = JsonObjectType.Object + }; + + return contentSchema; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs new file mode 100644 index 000000000..8fb749dc6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NJsonSchema; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +{ + public static class JsonSchemaExtensions + { + public static JsonSchema BuildJsonSchema(this Schema schema, PartitionResolver partitionResolver, SchemaResolver schemaResolver, bool withHidden = false) + { + Guard.NotNull(schemaResolver); + Guard.NotNull(partitionResolver); + + var schemaName = schema.Name.ToPascalCase(); + + var jsonTypeVisitor = new JsonTypeVisitor(schemaResolver, withHidden); + var jsonSchema = Builder.Object(); + + foreach (var field in schema.Fields.ForApi(withHidden)) + { + var partitionObject = Builder.Object(); + var partitionSet = partitionResolver(field.Partitioning); + + foreach (var partitionItem in partitionSet) + { + var partitionItemProperty = field.Accept(jsonTypeVisitor); + + if (partitionItemProperty != null) + { + partitionItemProperty.Description = partitionItem.Name; + partitionItemProperty.IsRequired = field.RawProperties.IsRequired && !partitionItem.IsOptional; + + partitionObject.Properties.Add(partitionItem.Key, partitionItemProperty); + } + } + + if (partitionObject.Properties.Count > 0) + { + var propertyReference = schemaResolver($"{schemaName}{field.Name.ToPascalCase()}Property", partitionObject); + + jsonSchema.Properties.Add(field.Name, CreateProperty(field, propertyReference)); + } + } + + return jsonSchema; + } + + public static JsonSchemaProperty CreateProperty(IField field, JsonSchema reference) + { + var jsonProperty = Builder.ObjectProperty(reference); + + if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints)) + { + jsonProperty.Description = $"{field.Name} ({field.RawProperties.Hints})"; + } + else + { + jsonProperty.Description = field.Name; + } + + jsonProperty.IsRequired = field.RawProperties.IsRequired; + + return jsonProperty; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs new file mode 100644 index 000000000..35ad7fe26 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs @@ -0,0 +1,151 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.ObjectModel; +using NJsonSchema; +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +{ + public delegate JsonSchema SchemaResolver(string name, JsonSchema schema); + + public sealed class JsonTypeVisitor : IFieldVisitor + { + private readonly SchemaResolver schemaResolver; + private readonly bool withHiddenFields; + + public JsonTypeVisitor(SchemaResolver schemaResolver, bool withHiddenFields) + { + this.schemaResolver = schemaResolver; + + this.withHiddenFields = withHiddenFields; + } + + public JsonSchemaProperty? Visit(IArrayField field) + { + var item = Builder.Object(); + + foreach (var nestedField in field.Fields.ForApi(withHiddenFields)) + { + var childProperty = nestedField.Accept(this); + + if (childProperty != null) + { + childProperty.Description = nestedField.RawProperties.Hints; + childProperty.IsRequired = nestedField.RawProperties.IsRequired; + + item.Properties.Add(nestedField.Name, childProperty); + } + } + + return Builder.ArrayProperty(item); + } + + public JsonSchemaProperty? Visit(IField field) + { + var item = schemaResolver("AssetItem", Builder.Guid()); + + return Builder.ArrayProperty(item); + } + + public JsonSchemaProperty? Visit(IField field) + { + return Builder.BooleanProperty(); + } + + public JsonSchemaProperty? Visit(IField field) + { + return Builder.DateTimeProperty(); + } + + public JsonSchemaProperty? Visit(IField field) + { + var geolocationSchema = Builder.Object(); + + geolocationSchema.Properties.Add("latitude", new JsonSchemaProperty + { + Type = JsonObjectType.Number, + Minimum = -90, + Maximum = 90, + IsRequired = true + }); + + geolocationSchema.Properties.Add("longitude", new JsonSchemaProperty + { + Type = JsonObjectType.Number, + Minimum = -180, + Maximum = 180, + IsRequired = true + }); + + var reference = schemaResolver("GeolocationDto", geolocationSchema); + + return Builder.ObjectProperty(reference); + } + + public JsonSchemaProperty? Visit(IField field) + { + return Builder.StringProperty(); + } + + public JsonSchemaProperty? Visit(IField field) + { + var property = Builder.NumberProperty(); + + if (field.Properties.MinValue.HasValue) + { + property.Minimum = (decimal)field.Properties.MinValue.Value; + } + + if (field.Properties.MaxValue.HasValue) + { + property.Maximum = (decimal)field.Properties.MaxValue.Value; + } + + return property; + } + + public JsonSchemaProperty? Visit(IField field) + { + var item = schemaResolver("ReferenceItem", Builder.Guid()); + + return Builder.ArrayProperty(item); + } + + public JsonSchemaProperty? Visit(IField field) + { + var property = Builder.StringProperty(); + + property.MinLength = field.Properties.MinLength; + property.MaxLength = field.Properties.MaxLength; + + if (field.Properties.AllowedValues != null) + { + var names = property.EnumerationNames ??= new Collection(); + + foreach (var value in field.Properties.AllowedValues) + { + names.Add(value); + } + } + + return property; + } + + public JsonSchemaProperty? Visit(IField field) + { + var item = schemaResolver("ReferenceItem", Builder.String()); + + return Builder.ArrayProperty(item); + } + + public JsonSchemaProperty? Visit(IField field) + { + return null; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Constants.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Constants.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/Constants.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Constants.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEventType.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEventType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEventType.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEventType.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedManualEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedManualEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedManualEvent.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedManualEvent.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventBase.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventBase.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventBase.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventBase.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventType.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventType.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventType.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs new file mode 100644 index 000000000..8872ef074 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Runtime.Serialization; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents +{ + public abstract class EnrichedUserEventBase : EnrichedEvent + { + public RefToken Actor { get; set; } + + [IgnoreDataMember] + public IUser? User { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/IEnrichedEntityEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/IEnrichedEntityEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/IEnrichedEntityEvent.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/IEnrichedEntityEvent.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs new file mode 100644 index 000000000..2f82c9480 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class EventEnricher : IEventEnricher + { + private static readonly TimeSpan UserCacheDuration = TimeSpan.FromMinutes(10); + private readonly IMemoryCache userCache; + private readonly IUserResolver userResolver; + + public EventEnricher(IMemoryCache userCache, IUserResolver userResolver) + { + Guard.NotNull(userCache); + Guard.NotNull(userResolver); + + this.userCache = userCache; + this.userResolver = userResolver; + } + + public async Task EnrichAsync(EnrichedEvent enrichedEvent, Envelope @event) + { + enrichedEvent.Timestamp = @event.Headers.Timestamp(); + + if (enrichedEvent is EnrichedUserEventBase userEvent) + { + if (@event.Payload is SquidexEvent squidexEvent) + { + userEvent.Actor = squidexEvent.Actor; + } + + userEvent.User = await FindUserAsync(userEvent.Actor); + } + + enrichedEvent.AppId = @event.Payload.AppId; + } + + private Task FindUserAsync(RefToken actor) + { + var key = $"EventEnrichers_Users_${actor.Identifier}"; + + return userCache.GetOrCreateAsync(key, async x => + { + x.AbsoluteExpirationRelativeToNow = UserCacheDuration; + + IUser? user; + try + { + user = await userResolver.FindByIdOrEmailAsync(actor.Identifier); + } + catch + { + user = null; + } + + if (user == null && actor.IsClient) + { + user = new ClientUser(actor); + } + + return user; + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs new file mode 100644 index 000000000..a74126cc6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public interface IRuleTriggerHandler + { + Type TriggerType { get; } + + Task CreateEnrichedEventAsync(Envelope @event); + + bool Trigger(EnrichedEvent @event, RuleTrigger trigger); + + bool Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs new file mode 100644 index 000000000..d90a41ac4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs @@ -0,0 +1,96 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Text; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class Result + { + public Exception? Exception { get; private set; } + + public string? Dump { get; private set; } + + public RuleResult Status { get; private set; } + + public void Enrich(TimeSpan elapsed) + { + var dumpBuilder = new StringBuilder(); + + if (!string.IsNullOrWhiteSpace(Dump)) + { + dumpBuilder.AppendLine(Dump); + } + + if (Status == RuleResult.Timeout) + { + dumpBuilder.AppendLine(); + dumpBuilder.AppendLine("Action timed out."); + } + + dumpBuilder.AppendLine(); + dumpBuilder.AppendFormat("Elapsed {0}.", elapsed); + dumpBuilder.AppendLine(); + + Dump = dumpBuilder.ToString(); + } + + public static Result Ignored() + { + return Success("Ignored"); + } + + public static Result Complete() + { + return Success("Completed"); + } + + public static Result Create(string? dump, RuleResult result) + { + return new Result { Dump = dump, Status = result }; + } + + public static Result Success(string? dump) + { + return new Result { Dump = dump, Status = RuleResult.Success }; + } + + public static Result Failed(Exception? ex) + { + return Failed(ex, ex?.Message); + } + + public static Result SuccessOrFailed(Exception? ex, string? dump) + { + if (ex != null) + { + return Failed(ex, dump); + } + else + { + return Success(dump); + } + } + + public static Result Failed(Exception? ex, string? dump) + { + var result = new Result { Exception = ex, Dump = dump ?? ex?.Message }; + + if (ex is OperationCanceledException || ex is TimeoutException) + { + result.Status = RuleResult.Timeout; + } + else + { + result.Status = RuleResult.Failed; + } + + return result; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs new file mode 100644 index 000000000..c2b62a052 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs @@ -0,0 +1,86 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure; + +#pragma warning disable RECS0083 // Shows NotImplementedException throws in the quick task bar + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public abstract class RuleActionHandler : IRuleActionHandler where TAction : RuleAction + { + private readonly RuleEventFormatter formatter; + + Type IRuleActionHandler.ActionType + { + get { return typeof(TAction); } + } + + Type IRuleActionHandler.DataType + { + get { return typeof(TData); } + } + + protected RuleActionHandler(RuleEventFormatter formatter) + { + Guard.NotNull(formatter); + + this.formatter = formatter; + } + + protected virtual string ToJson(T @event) + { + return formatter.ToPayload(@event); + } + + protected virtual string ToEnvelopeJson(EnrichedEvent @event) + { + return formatter.ToEnvelope(@event); + } + + protected string? Format(Uri uri, EnrichedEvent @event) + { + return formatter.Format(uri.ToString(), @event); + } + + protected string? Format(string text, EnrichedEvent @event) + { + return formatter.Format(text, @event); + } + + async Task<(string Description, object Data)> IRuleActionHandler.CreateJobAsync(EnrichedEvent @event, RuleAction action) + { + var (description, data) = await CreateJobAsync(@event, (TAction)action); + + return (description, data!); + } + + async Task IRuleActionHandler.ExecuteJobAsync(object data, CancellationToken ct) + { + var typedData = (TData)data; + + return await ExecuteJobAsync(typedData, ct); + } + + protected virtual Task<(string Description, TData Data)> CreateJobAsync(EnrichedEvent @event, TAction action) + { + return Task.FromResult(CreateJob(@event, action)); + } + + protected virtual (string Description, TData Data) CreateJob(EnrichedEvent @event, TAction action) + { + throw new NotImplementedException(); + } + + protected abstract Task ExecuteJobAsync(TData job, CancellationToken ct = default); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs new file mode 100644 index 000000000..956314daa --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class RuleActionProperty + { + public RuleActionPropertyEditor Editor { get; set; } + + public string Name { get; set; } + + public string Display { get; set; } + + public string? Description { get; set; } + + public bool IsFormattable { get; set; } + + public bool IsRequired { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs new file mode 100644 index 000000000..e90411d69 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class RuleActionRegistration + { + public Type ActionType { get; } + + internal RuleActionRegistration(Type actionType) + { + Guard.NotNull(actionType); + + ActionType = actionType; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs new file mode 100644 index 000000000..7e2ee9b19 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -0,0 +1,314 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// =========================================-================================= + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public class RuleEventFormatter + { + private const string Fallback = "null"; + private const string ScriptSuffix = ")"; + private const string ScriptPrefix = "Script("; + private static readonly char[] ContentPlaceholderStartOld = "CONTENT_DATA".ToCharArray(); + private static readonly char[] ContentPlaceholderStartNew = "{CONTENT_DATA".ToCharArray(); + private static readonly Regex ContentDataPlaceholderOld = new Regex(@"^CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled); + private static readonly Regex ContentDataPlaceholderNew = new Regex(@"^\{CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}\}", RegexOptions.Compiled); + private readonly List<(char[] Pattern, Func Replacer)> patterns = new List<(char[] Pattern, Func Replacer)>(); + private readonly IJsonSerializer jsonSerializer; + private readonly IRuleUrlGenerator urlGenerator; + private readonly IScriptEngine scriptEngine; + + public RuleEventFormatter(IJsonSerializer jsonSerializer, IRuleUrlGenerator urlGenerator, IScriptEngine scriptEngine) + { + Guard.NotNull(jsonSerializer); + Guard.NotNull(scriptEngine); + Guard.NotNull(urlGenerator); + + this.jsonSerializer = jsonSerializer; + this.scriptEngine = scriptEngine; + this.urlGenerator = urlGenerator; + + AddPattern("APP_ID", AppId); + AddPattern("APP_NAME", AppName); + AddPattern("CONTENT_ACTION", ContentAction); + AddPattern("CONTENT_STATUS", ContentStatus); + AddPattern("CONTENT_URL", ContentUrl); + AddPattern("SCHEMA_ID", SchemaId); + AddPattern("SCHEMA_NAME", SchemaName); + AddPattern("TIMESTAMP_DATETIME", TimestampTime); + AddPattern("TIMESTAMP_DATE", TimestampDate); + AddPattern("USER_ID", UserId); + AddPattern("USER_NAME", UserName); + AddPattern("USER_EMAIL", UserEmail); + } + + private void AddPattern(string placeholder, Func generator) + { + patterns.Add((placeholder.ToCharArray(), generator)); + } + + public virtual string ToPayload(T @event) + { + return jsonSerializer.Serialize(@event); + } + + public virtual string ToEnvelope(EnrichedEvent @event) + { + return jsonSerializer.Serialize(new { type = @event.Name, payload = @event, timestamp = @event.Timestamp }); + } + + public string? Format(string text, EnrichedEvent @event) + { + if (string.IsNullOrWhiteSpace(text)) + { + return text; + } + + var trimmed = text.Trim(); + + if (trimmed.StartsWith(ScriptPrefix, StringComparison.OrdinalIgnoreCase) && trimmed.EndsWith(ScriptSuffix, StringComparison.OrdinalIgnoreCase)) + { + var script = trimmed.Substring(ScriptPrefix.Length, trimmed.Length - ScriptPrefix.Length - ScriptSuffix.Length); + + var customFunctions = new Dictionary> + { + ["contentUrl"] = () => ContentUrl(@event), + ["contentAction"] = () => ContentAction(@event) + }; + + return scriptEngine.Interpolate("event", @event, script, customFunctions); + } + + var current = text.AsSpan(); + + var sb = new StringBuilder(); + + var cp2 = new ReadOnlySpan(ContentPlaceholderStartNew); + var cp1 = new ReadOnlySpan(ContentPlaceholderStartOld); + + for (var i = 0; i < current.Length; i++) + { + var c = current[i]; + + if (c == '$') + { + sb.Append(current.Slice(0, i).ToString()); + + current = current.Slice(i); + + var test = current.Slice(1); + var tested = false; + + for (var j = 0; j < patterns.Count; j++) + { + var (pattern, replacer) = patterns[j]; + + if (test.StartsWith(pattern, StringComparison.OrdinalIgnoreCase)) + { + sb.Append(replacer(@event)); + + current = current.Slice(pattern.Length + 1); + i = 0; + + tested = true; + break; + } + } + + if (!tested && (test.StartsWith(cp1, StringComparison.OrdinalIgnoreCase) || test.StartsWith(cp2, StringComparison.OrdinalIgnoreCase))) + { + var currentString = test.ToString(); + + var match = ContentDataPlaceholderOld.Match(currentString); + + if (!match.Success) + { + match = ContentDataPlaceholderNew.Match(currentString); + } + + if (match.Success) + { + if (@event is EnrichedContentEvent contentEvent) + { + sb.Append(CalculateData(contentEvent.Data, match)); + } + else + { + sb.Append(Fallback); + } + + current = current.Slice(match.Length + 1); + i = 0; + } + } + } + } + + sb.Append(current.ToString()); + + return sb.ToString(); + } + + private static string TimestampDate(EnrichedEvent @event) + { + return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd", CultureInfo.InvariantCulture); + } + + private static string TimestampTime(EnrichedEvent @event) + { + return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd-hh-mm-ss", CultureInfo.InvariantCulture); + } + + private static string AppId(EnrichedEvent @event) + { + return @event.AppId.Id.ToString(); + } + + private static string AppName(EnrichedEvent @event) + { + return @event.AppId.Name; + } + + private static string SchemaId(EnrichedEvent @event) + { + if (@event is EnrichedSchemaEventBase schemaEvent) + { + return schemaEvent.SchemaId.Id.ToString(); + } + + return Fallback; + } + + private static string SchemaName(EnrichedEvent @event) + { + if (@event is EnrichedSchemaEventBase schemaEvent) + { + return schemaEvent.SchemaId.Name; + } + + return Fallback; + } + + private static string ContentAction(EnrichedEvent @event) + { + if (@event is EnrichedContentEvent contentEvent) + { + return contentEvent.Type.ToString(); + } + + return Fallback; + } + + private static string ContentStatus(EnrichedEvent @event) + { + if (@event is EnrichedContentEvent contentEvent) + { + return contentEvent.Status.ToString(); + } + + return Fallback; + } + + private string ContentUrl(EnrichedEvent @event) + { + if (@event is EnrichedContentEvent contentEvent) + { + return urlGenerator.GenerateContentUIUrl(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); + } + + return Fallback; + } + + private static string UserName(EnrichedEvent @event) + { + if (@event is EnrichedUserEventBase userEvent) + { + return userEvent.User?.DisplayName() ?? Fallback; + } + + return Fallback; + } + + private static string UserId(EnrichedEvent @event) + { + if (@event is EnrichedUserEventBase userEvent) + { + return userEvent.User?.Id ?? Fallback; + } + + return Fallback; + } + + private static string UserEmail(EnrichedEvent @event) + { + if (@event is EnrichedUserEventBase userEvent) + { + return userEvent.User?.Email ?? Fallback; + } + + return Fallback; + } + + private static string CalculateData(NamedContentData data, Match match) + { + var captures = match.Groups[2].Captures; + + var path = new string[captures.Count]; + + for (var i = 0; i < path.Length; i++) + { + path[i] = captures[i].Value; + } + + if (!data.TryGetValue(path[0], out var field) || field == null) + { + return Fallback; + } + + if (!field.TryGetValue(path[1], out var value)) + { + return Fallback; + } + + for (var j = 2; j < path.Length; j++) + { + if (value is JsonObject obj && obj.TryGetValue(path[j], out value)) + { + continue; + } + + if (value is JsonArray array && int.TryParse(path[j], out var idx) && idx >= 0 && idx < array.Count) + { + value = array[idx]; + } + else + { + return Fallback; + } + } + + if (value == null || value.Type == JsonValueType.Null) + { + return Fallback; + } + + return value.ToString() ?? Fallback; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleOptions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleOptions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleOptions.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleOptions.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs new file mode 100644 index 000000000..bb828c223 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs @@ -0,0 +1,189 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; + +#pragma warning disable RECS0033 // Convert 'if' to '||' expression + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class RuleRegistry : ITypeProvider + { + private const string ActionSuffix = "Action"; + private const string ActionSuffixV2 = "ActionV2"; + private readonly Dictionary actionTypes = new Dictionary(); + + public IReadOnlyDictionary Actions + { + get { return actionTypes; } + } + + public RuleRegistry(IEnumerable? registrations = null) + { + if (registrations != null) + { + foreach (var registration in registrations) + { + Add(registration.ActionType); + } + } + } + + public void Add() where T : RuleAction + { + Add(typeof(T)); + } + + private void Add(Type actionType) + { + var metadata = actionType.GetCustomAttribute(); + + if (metadata == null) + { + return; + } + + var name = GetActionName(actionType); + + var definition = + new RuleActionDefinition + { + Type = actionType, + Title = metadata.Title, + Display = metadata.Display, + Description = metadata.Description, + IconColor = metadata.IconColor, + IconImage = metadata.IconImage, + ReadMore = metadata.ReadMore + }; + + foreach (var property in actionType.GetProperties()) + { + if (property.CanRead && property.CanWrite) + { + var actionProperty = new RuleActionProperty { Name = property.Name.ToCamelCase(), Display = property.Name }; + + var display = property.GetCustomAttribute(); + + if (!string.IsNullOrWhiteSpace(display?.Name)) + { + actionProperty.Display = display.Name; + } + + if (!string.IsNullOrWhiteSpace(display?.Description)) + { + actionProperty.Description = display.Description; + } + + var type = property.PropertyType; + + if ((GetDataAttribute(property) != null || (type.IsValueType && !IsNullable(type))) && type != typeof(bool) && type != typeof(bool?)) + { + actionProperty.IsRequired = true; + } + + if (property.GetCustomAttribute() != null) + { + actionProperty.IsFormattable = true; + } + + var dataType = GetDataAttribute(property)?.DataType; + + if (type == typeof(bool) || type == typeof(bool?)) + { + actionProperty.Editor = RuleActionPropertyEditor.Checkbox; + } + else if (type == typeof(int) || type == typeof(int?)) + { + actionProperty.Editor = RuleActionPropertyEditor.Number; + } + else if (dataType == DataType.Url) + { + actionProperty.Editor = RuleActionPropertyEditor.Url; + } + else if (dataType == DataType.Password) + { + actionProperty.Editor = RuleActionPropertyEditor.Password; + } + else if (dataType == DataType.EmailAddress) + { + actionProperty.Editor = RuleActionPropertyEditor.Email; + } + else if (dataType == DataType.MultilineText) + { + actionProperty.Editor = RuleActionPropertyEditor.TextArea; + } + else + { + actionProperty.Editor = RuleActionPropertyEditor.Text; + } + + definition.Properties.Add(actionProperty); + } + } + + actionTypes[name] = definition; + } + + private static T? GetDataAttribute(PropertyInfo property) where T : ValidationAttribute + { + var result = property.GetCustomAttribute(); + + result?.IsValid(null); + + return result; + } + + private static bool IsNullable(Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + + private static string GetActionName(Type type) + { + return type.TypeName(false, ActionSuffix, ActionSuffixV2); + } + + public void Map(TypeNameRegistry typeNameRegistry) + { + foreach (var actionType in actionTypes.Values) + { + typeNameRegistry.Map(actionType.Type, actionType.Type.Name); + } + + var eventTypes = typeof(EnrichedEvent).Assembly.GetTypes().Where(x => typeof(EnrichedEvent).IsAssignableFrom(x) && !x.IsAbstract); + + var addedTypes = new HashSet(); + + foreach (var type in eventTypes) + { + if (addedTypes.Add(type)) + { + typeNameRegistry.Map(type, type.Name); + } + } + + var triggerTypes = typeof(RuleTrigger).Assembly.GetTypes().Where(x => typeof(RuleTrigger).IsAssignableFrom(x) && !x.IsAbstract); + + foreach (var type in triggerTypes) + { + if (addedTypes.Add(type)) + { + typeNameRegistry.Map(type, type.Name); + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs new file mode 100644 index 000000000..cb2f21066 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -0,0 +1,202 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using NodaTime; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public class RuleService + { + private readonly Dictionary ruleActionHandlers; + private readonly Dictionary ruleTriggerHandlers; + private readonly TypeNameRegistry typeNameRegistry; + private readonly RuleOptions ruleOptions; + private readonly IEventEnricher eventEnricher; + private readonly IJsonSerializer jsonSerializer; + private readonly IClock clock; + private readonly ISemanticLog log; + + public RuleService( + IOptions ruleOptions, + IEnumerable ruleTriggerHandlers, + IEnumerable ruleActionHandlers, + IEventEnricher eventEnricher, + IJsonSerializer jsonSerializer, + IClock clock, + ISemanticLog log, + TypeNameRegistry typeNameRegistry) + { + Guard.NotNull(jsonSerializer); + Guard.NotNull(ruleOptions); + Guard.NotNull(ruleTriggerHandlers); + Guard.NotNull(ruleActionHandlers); + Guard.NotNull(typeNameRegistry); + Guard.NotNull(eventEnricher); + Guard.NotNull(clock); + Guard.NotNull(log); + + this.typeNameRegistry = typeNameRegistry; + + this.ruleOptions = ruleOptions.Value; + this.ruleTriggerHandlers = ruleTriggerHandlers.ToDictionary(x => x.TriggerType); + this.ruleActionHandlers = ruleActionHandlers.ToDictionary(x => x.ActionType); + this.eventEnricher = eventEnricher; + + this.jsonSerializer = jsonSerializer; + + this.clock = clock; + + this.log = log; + } + + public virtual async Task CreateJobAsync(Rule rule, Guid ruleId, Envelope @event) + { + Guard.NotNull(rule); + Guard.NotNull(@event); + + try + { + if (!rule.IsEnabled) + { + return null; + } + + if (!(@event.Payload is AppEvent)) + { + return null; + } + + var typed = @event.To(); + + var actionType = rule.Action.GetType(); + + if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) + { + return null; + } + + if (!ruleActionHandlers.TryGetValue(actionType, out var actionHandler)) + { + return null; + } + + var now = clock.GetCurrentInstant(); + + var eventTime = + @event.Headers.ContainsKey(CommonHeaders.Timestamp) ? + @event.Headers.Timestamp() : + now; + + var expires = eventTime.Plus(Constants.ExpirationTime); + + if (eventTime.Plus(Constants.StaleTime) < now) + { + return null; + } + + if (!triggerHandler.Trigger(typed.Payload, rule.Trigger, ruleId)) + { + return null; + } + + var appEventEnvelope = @event.To(); + + var enrichedEvent = await triggerHandler.CreateEnrichedEventAsync(appEventEnvelope); + + if (enrichedEvent == null) + { + return null; + } + + await eventEnricher.EnrichAsync(enrichedEvent, typed); + + if (!triggerHandler.Trigger(enrichedEvent, rule.Trigger)) + { + return null; + } + + var actionName = typeNameRegistry.GetName(actionType); + var actionData = await actionHandler.CreateJobAsync(enrichedEvent, rule.Action); + + var json = jsonSerializer.Serialize(actionData.Data); + + var job = new RuleJob + { + Id = Guid.NewGuid(), + ActionData = json, + ActionName = actionName, + AppId = enrichedEvent.AppId.Id, + Created = now, + Description = actionData.Description, + EventName = enrichedEvent.Name, + ExecutionPartition = enrichedEvent.Partition, + Expires = expires, + RuleId = ruleId + }; + + return job; + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "createRuleJob") + .WriteProperty("status", "Failed")); + + return null; + } + } + + public virtual async Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job) + { + var actionWatch = ValueStopwatch.StartNew(); + + Result result; + + try + { + var actionType = typeNameRegistry.GetType(actionName); + var actionHandler = ruleActionHandlers[actionType]; + + var deserialized = jsonSerializer.Deserialize(job, actionHandler.DataType); + + using (var cts = new CancellationTokenSource(GetTimeoutInMs())) + { + result = await actionHandler.ExecuteJobAsync(deserialized, cts.Token).WithCancellation(cts.Token); + } + } + catch (Exception ex) + { + result = Result.Failed(ex); + } + + var elapsed = TimeSpan.FromMilliseconds(actionWatch.Stop()); + + result.Enrich(elapsed); + + return (result, elapsed); + } + + private int GetTimeoutInMs() + { + return ruleOptions.ExecutionTimeoutInSeconds * 1000; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs new file mode 100644 index 000000000..5a533e5c1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.EventSourcing; + +#pragma warning disable IDE0019 // Use pattern matching + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public abstract class RuleTriggerHandler : IRuleTriggerHandler + where TTrigger : RuleTrigger + where TEvent : AppEvent + where TEnrichedEvent : EnrichedEvent + { + public Type TriggerType + { + get { return typeof(TTrigger); } + } + + async Task IRuleTriggerHandler.CreateEnrichedEventAsync(Envelope @event) + { + return await CreateEnrichedEventAsync(@event.To()); + } + + bool IRuleTriggerHandler.Trigger(EnrichedEvent @event, RuleTrigger trigger) + { + if (@event is TEnrichedEvent typed) + { + return Trigger(typed, (TTrigger)trigger); + } + + return false; + } + + bool IRuleTriggerHandler.Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId) + { + if (@event is TEvent typed) + { + return Trigger(typed, (TTrigger)trigger, ruleId); + } + + return false; + } + + protected abstract Task CreateEnrichedEventAsync(Envelope @event); + + protected abstract bool Trigger(TEnrichedEvent @event, TTrigger trigger); + + protected virtual bool Trigger(TEvent @event, TTrigger trigger, Guid ruleId) + { + return true; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs new file mode 100644 index 000000000..7ba4be400 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs @@ -0,0 +1,130 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Jint; +using Jint.Native; +using Jint.Native.Object; +using Jint.Runtime.Descriptors; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +#pragma warning disable RECS0133 // Parameter name differs in base declaration + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public sealed class ContentDataObject : ObjectInstance + { + private readonly NamedContentData contentData; + private HashSet fieldsToDelete; + private Dictionary fieldProperties; + private bool isChanged; + + public ContentDataObject(Engine engine, NamedContentData contentData) + : base(engine) + { + Extensible = true; + + this.contentData = contentData; + } + + public void MarkChanged() + { + isChanged = true; + } + + public bool TryUpdate(out NamedContentData result) + { + result = contentData; + + if (isChanged) + { + if (fieldsToDelete != null) + { + foreach (var field in fieldsToDelete) + { + contentData.Remove(field); + } + } + + if (fieldProperties != null) + { + foreach (var kvp in fieldProperties) + { + var value = (ContentDataProperty)kvp.Value; + + if (value.ContentField != null && value.ContentField.TryUpdate(out var fieldData)) + { + contentData[kvp.Key] = fieldData; + } + } + } + } + + return isChanged; + } + + public override void RemoveOwnProperty(string propertyName) + { + if (fieldsToDelete == null) + { + fieldsToDelete = new HashSet(); + } + + fieldsToDelete.Add(propertyName); + fieldProperties?.Remove(propertyName); + + MarkChanged(); + } + + public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError) + { + EnsurePropertiesInitialized(); + + if (!fieldProperties.ContainsKey(propertyName)) + { + fieldProperties[propertyName] = new ContentDataProperty(this) { Value = desc.Value }; + } + + return true; + } + + public override void Put(string propertyName, JsValue value, bool throwOnError) + { + EnsurePropertiesInitialized(); + + fieldProperties.GetOrAdd(propertyName, this, (k, c) => new ContentDataProperty(c)).Value = value; + } + + public override PropertyDescriptor GetOwnProperty(string propertyName) + { + EnsurePropertiesInitialized(); + + return fieldProperties.GetOrAdd(propertyName, this, (k, c) => new ContentDataProperty(c, new ContentFieldObject(c, new ContentFieldData(), false))); + } + + public override IEnumerable> GetOwnProperties() + { + EnsurePropertiesInitialized(); + + return fieldProperties; + } + + private void EnsurePropertiesInitialized() + { + if (fieldProperties == null) + { + fieldProperties = new Dictionary(contentData.Count); + + foreach (var kvp in contentData) + { + fieldProperties.Add(kvp.Key, new ContentDataProperty(this, new ContentFieldObject(this, kvp.Value, false))); + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs new file mode 100644 index 000000000..eeeb96519 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Jint.Native; +using Jint.Runtime; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public sealed class ContentDataProperty : CustomProperty + { + private readonly ContentDataObject contentData; + private ContentFieldObject? contentField; + private JsValue value; + + protected override JsValue CustomValue + { + get + { + return value; + } + set + { + if (!Equals(this.value, value)) + { + if (value == null || !value.IsObject()) + { + throw new JavaScriptException("You can only assign objects to content data."); + } + + var obj = value.AsObject(); + + contentField = new ContentFieldObject(contentData, new ContentFieldData(), true); + + foreach (var kvp in obj.GetOwnProperties()) + { + contentField.Put(kvp.Key, kvp.Value.Value, true); + } + + this.value = contentField; + } + } + } + + public ContentFieldObject? ContentField + { + get { return contentField; } + } + + public ContentDataProperty(ContentDataObject contentData, ContentFieldObject? contentField = null) + { + this.contentData = contentData; + this.contentField = contentField; + + if (contentField != null) + { + value = contentField; + } + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs new file mode 100644 index 000000000..79b54ce2f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs @@ -0,0 +1,138 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Jint.Native.Object; +using Jint.Runtime.Descriptors; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +#pragma warning disable RECS0133 // Parameter name differs in base declaration + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public sealed class ContentFieldObject : ObjectInstance + { + private readonly ContentDataObject contentData; + private readonly ContentFieldData? fieldData; + private HashSet valuesToDelete; + private Dictionary valueProperties; + private bool isChanged; + + public ContentFieldData? FieldData + { + get { return fieldData; } + } + + public ContentFieldObject(ContentDataObject contentData, ContentFieldData? fieldData, bool isNew) + : base(contentData.Engine) + { + Extensible = true; + + this.contentData = contentData; + this.fieldData = fieldData; + + if (isNew) + { + MarkChanged(); + } + } + + public void MarkChanged() + { + isChanged = true; + + contentData.MarkChanged(); + } + + public bool TryUpdate(out ContentFieldData? result) + { + result = fieldData; + + if (isChanged && fieldData != null) + { + if (valuesToDelete != null) + { + foreach (var field in valuesToDelete) + { + fieldData.Remove(field); + } + } + + if (valueProperties != null) + { + foreach (var kvp in valueProperties) + { + var value = (ContentFieldProperty)kvp.Value; + + if (value.IsChanged) + { + fieldData[kvp.Key] = value.ContentValue; + } + } + } + } + + return isChanged; + } + + public override void RemoveOwnProperty(string propertyName) + { + if (valuesToDelete == null) + { + valuesToDelete = new HashSet(); + } + + valuesToDelete.Add(propertyName); + valueProperties?.Remove(propertyName); + + MarkChanged(); + } + + public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError) + { + EnsurePropertiesInitialized(); + + if (!valueProperties.ContainsKey(propertyName)) + { + valueProperties[propertyName] = new ContentFieldProperty(this) { Value = desc.Value }; + } + + return true; + } + + public override PropertyDescriptor GetOwnProperty(string propertyName) + { + EnsurePropertiesInitialized(); + + return valueProperties?.GetOrDefault(propertyName) ?? PropertyDescriptor.Undefined; + } + + public override IEnumerable> GetOwnProperties() + { + EnsurePropertiesInitialized(); + + return valueProperties; + } + + private void EnsurePropertiesInitialized() + { + if (valueProperties == null) + { + valueProperties = new Dictionary(fieldData?.Count ?? 0); + + if (fieldData != null) + { + foreach (var kvp in fieldData) + { + valueProperties.Add(kvp.Key, new ContentFieldProperty(this, kvp.Value)); + } + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs new file mode 100644 index 000000000..3136806e7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Jint.Native; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public sealed class ContentFieldProperty : CustomProperty + { + private readonly ContentFieldObject contentField; + private IJsonValue? contentValue; + private JsValue? value; + private bool isChanged; + + protected override JsValue? CustomValue + { + get + { + if (value == null) + { + if (contentValue != null) + { + value = JsonMapper.Map(contentValue, contentField.Engine); + } + } + + return value; + } + set + { + if (!Equals(this.value, value)) + { + this.value = value; + + contentValue = null; + contentField.MarkChanged(); + + isChanged = true; + } + } + } + + public IJsonValue ContentValue + { + get { return contentValue ?? (contentValue = JsonMapper.Map(value)); } + } + + public bool IsChanged + { + get { return isChanged; } + } + + public ContentFieldProperty(ContentFieldObject contentField, IJsonValue? contentValue = null) + { + this.contentField = contentField; + this.contentValue = contentValue; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs new file mode 100644 index 000000000..20c15d600 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs @@ -0,0 +1,131 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Jint; +using Jint.Native; +using Jint.Native.Object; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public static class JsonMapper + { + public static JsValue Map(IJsonValue? value, Engine engine) + { + if (value == null) + { + return JsValue.Null; + } + + switch (value) + { + case JsonNull _: + return JsValue.Null; + case JsonScalar s: + return new JsString(s.Value); + case JsonScalar b: + return new JsBoolean(b.Value); + case JsonScalar b: + return new JsNumber(b.Value); + case JsonObject obj: + return FromObject(obj, engine); + case JsonArray arr: + return FromArray(arr, engine); + } + + throw new ArgumentException("Invalid json type.", nameof(value)); + } + + private static JsValue FromArray(JsonArray arr, Engine engine) + { + var target = new JsValue[arr.Count]; + + for (var i = 0; i < arr.Count; i++) + { + target[i] = Map(arr[i], engine); + } + + return engine.Array.Construct(target); + } + + private static JsValue FromObject(JsonObject obj, Engine engine) + { + var target = new ObjectInstance(engine); + + foreach (var property in obj) + { + target.FastAddProperty(property.Key, Map(property.Value, engine), false, true, true); + } + + return target; + } + + public static IJsonValue Map(JsValue? value) + { + if (value == null || value.IsNull() || value.IsUndefined()) + { + return JsonValue.Null; + } + + if (value.IsString()) + { + return JsonValue.Create(value.AsString()); + } + + if (value.IsBoolean()) + { + return JsonValue.Create(value.AsBoolean()); + } + + if (value.IsNumber()) + { + return JsonValue.Create(value.AsNumber()); + } + + if (value.IsDate()) + { + return JsonValue.Create(value.AsDate().ToString()); + } + + if (value.IsRegExp()) + { + return JsonValue.Create(value.AsRegExp().Value?.ToString()); + } + + if (value.IsArray()) + { + var arr = value.AsArray(); + + var result = JsonValue.Array(); + + for (var i = 0; i < arr.GetLength(); i++) + { + result.Add(Map(arr.Get(i.ToString()))); + } + + return result; + } + + if (value.IsObject()) + { + var obj = value.AsObject(); + + var result = JsonValue.Object(); + + foreach (var kvp in obj.GetOwnProperties()) + { + result[kvp.Key] = Map(kvp.Value.Value); + } + + return result; + } + + throw new ArgumentException("Invalid json type.", nameof(value)); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs new file mode 100644 index 000000000..b65a10901 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Security.Claims; +using Jint; +using Jint.Native; +using Jint.Runtime.Interop; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public sealed class DefaultConverter : IObjectConverter + { + public static readonly DefaultConverter Instance = new DefaultConverter(); + + private DefaultConverter() + { + } + + public bool TryConvert(Engine engine, object value, [MaybeNullWhen(false)] out JsValue result) + { + result = null!; + + if (value is Enum) + { + result = value.ToString(); + return true; + } + + switch (value) + { + case IUser user: + result = JintUser.Create(engine, user); + return true; + case ClaimsPrincipal principal: + result = JintUser.Create(engine, principal); + return true; + case Instant instant: + result = JsValue.FromObject(engine, instant.ToDateTimeUtc()); + return true; + case Status status: + result = status.ToString(); + return true; + case NamedContentData content: + result = new ContentDataObject(engine, content); + return true; + } + + return false; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs new file mode 100644 index 000000000..79883257d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public interface IScriptEngine + { + void Execute(ScriptContext context, string script); + + NamedContentData ExecuteAndTransform(ScriptContext context, string script); + + NamedContentData Transform(ScriptContext context, string script); + + bool Evaluate(string name, object context, string script); + + string? Interpolate(string name, object context, string script, Dictionary>? customFormatters = null); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs new file mode 100644 index 000000000..2510d9287 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -0,0 +1,312 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Globalization; +using Esprima; +using Jint; +using Jint.Native; +using Jint.Native.Date; +using Jint.Native.Object; +using Jint.Runtime; +using Jint.Runtime.Interop; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public sealed class JintScriptEngine : IScriptEngine + { + public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200); + + public void Execute(ScriptContext context, string script) + { + Guard.NotNull(context); + + if (!string.IsNullOrWhiteSpace(script)) + { + var engine = CreateScriptEngine(context); + + EnableDisallow(engine); + EnableReject(engine); + + Execute(engine, script); + } + } + + public NamedContentData ExecuteAndTransform(ScriptContext context, string script) + { + Guard.NotNull(context); + + var result = context.Data!; + + if (!string.IsNullOrWhiteSpace(script)) + { + var engine = CreateScriptEngine(context); + + EnableDisallow(engine); + EnableReject(engine); + + engine.SetValue("operation", new Action(() => + { + var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); + + if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) + { + data.TryUpdate(out result); + } + })); + + engine.SetValue("replace", new Action(() => + { + var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); + + if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) + { + data.TryUpdate(out result); + } + })); + + Execute(engine, script); + } + + return result; + } + + public NamedContentData Transform(ScriptContext context, string script) + { + Guard.NotNull(context); + + var result = context.Data!; + + if (!string.IsNullOrWhiteSpace(script)) + { + try + { + var engine = CreateScriptEngine(context); + + engine.SetValue("replace", new Action(() => + { + var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); + + if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) + { + data.TryUpdate(out result); + } + })); + + engine.Execute(script); + } + catch (Exception) + { + result = context.Data!; + } + } + + return result; + } + + private static void Execute(Engine engine, string script) + { + try + { + engine.Execute(script); + } + catch (ArgumentException ex) + { + throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message)); + } + catch (JavaScriptException ex) + { + throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message)); + } + catch (ParserException ex) + { + throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message)); + } + } + + private Engine CreateScriptEngine(ScriptContext context) + { + var engine = CreateScriptEngine(); + + var contextInstance = new ObjectInstance(engine); + + if (context.Data != null) + { + contextInstance.FastAddProperty("data", new ContentDataObject(engine, context.Data), true, true, true); + } + + if (context.DataOld != null) + { + contextInstance.FastAddProperty("oldData", new ContentDataObject(engine, context.DataOld), true, true, true); + } + + if (context.User != null) + { + contextInstance.FastAddProperty("user", JintUser.Create(engine, context.User), false, true, false); + } + + if (!string.IsNullOrWhiteSpace(context.Operation)) + { + contextInstance.FastAddProperty("operation", context.Operation, false, false, false); + } + + contextInstance.FastAddProperty("status", context.Status.ToString(), false, false, false); + + if (context.StatusOld != default) + { + contextInstance.FastAddProperty("oldStatus", context.StatusOld.ToString(), false, false, false); + } + + engine.SetValue("ctx", contextInstance); + engine.SetValue("context", contextInstance); + + return engine; + } + + private Engine CreateScriptEngine(IReferenceResolver? resolver = null, Dictionary>? customFormatters = null) + { + var engine = new Engine(options => + { + if (resolver != null) + { + options.SetReferencesResolver(resolver); + } + + options.TimeoutInterval(Timeout).Strict().AddObjectConverter(DefaultConverter.Instance); + }); + + if (customFormatters != null) + { + foreach (var kvp in customFormatters) + { + engine.SetValue(kvp.Key, Safe(kvp.Value)); + } + } + + engine.SetValue("slugify", new ClrFunctionInstance(engine, "slugify", Slugify)); + engine.SetValue("formatTime", new ClrFunctionInstance(engine, "formatTime", FormatDate)); + engine.SetValue("formatDate", new ClrFunctionInstance(engine, "formatDate", FormatDate)); + + return engine; + } + + private static Func Safe(Func func) + { + return () => + { + try + { + return func(); + } + catch + { + return "null"; + } + }; + } + + private static JsValue Slugify(JsValue thisObject, JsValue[] arguments) + { + try + { + var stringInput = TypeConverter.ToString(arguments.At(0)); + var single = false; + + if (arguments.Length > 1) + { + single = TypeConverter.ToBoolean(arguments.At(1)); + } + + return stringInput.Slugify(null, single); + } + catch + { + return JsValue.Undefined; + } + } + + private static JsValue FormatDate(JsValue thisObject, JsValue[] arguments) + { + try + { + var dateValue = ((DateInstance)arguments.At(0)).ToDateTime(); + var dateFormat = TypeConverter.ToString(arguments.At(1)); + + return dateValue.ToString(dateFormat, CultureInfo.InvariantCulture); + } + catch + { + return JsValue.Undefined; + } + } + + private static void EnableDisallow(Engine engine) + { + engine.SetValue("disallow", new Action(message => + { + var exMessage = !string.IsNullOrWhiteSpace(message) ? message : "Not allowed"; + + throw new DomainForbiddenException(exMessage); + })); + } + + private static void EnableReject(Engine engine) + { + engine.SetValue("reject", new Action(message => + { + var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null; + + throw new ValidationException("Script rejected the operation.", errors); + })); + } + + public bool Evaluate(string name, object context, string script) + { + try + { + var result = + CreateScriptEngine(NullPropagation.Instance) + .SetValue(name, context) + .Execute(script) + .GetCompletionValue() + .ToObject(); + + return (bool)result; + } + catch + { + return false; + } + } + + public string? Interpolate(string name, object context, string script, Dictionary>? customFormatters = null) + { + try + { + var result = + CreateScriptEngine(NullPropagation.Instance, customFormatters) + .SetValue(name, context) + .Execute(script) + .GetCompletionValue() + .ToObject(); + + var converted = result.ToString(); + + return converted == "undefined" ? "null" : converted; + } + catch (Exception ex) + { + return ex.Message; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs new file mode 100644 index 000000000..a0725cbe5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs @@ -0,0 +1,59 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Jint; +using Jint.Runtime.Interop; +using Squidex.Infrastructure.Security; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public static class JintUser + { + private static readonly char[] ClaimSeparators = { '/', '.', ':' }; + + public static ObjectWrapper Create(Engine engine, IUser user) + { + var clientId = user.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value; + + var isClient = !string.IsNullOrWhiteSpace(clientId); + + return CreateUser(engine, user.Id, isClient, user.Email, user.DisplayName(), user.Claims); + } + + public static ObjectWrapper Create(Engine engine, ClaimsPrincipal principal) + { + var id = principal.OpenIdSubject()!; + + var isClient = string.IsNullOrWhiteSpace(id); + + if (isClient) + { + id = principal.OpenIdClientId()!; + } + + var name = principal.FindFirst(SquidexClaimTypes.DisplayName)?.Value; + + return CreateUser(engine, id, isClient, principal.OpenIdEmail()!, name, principal.Claims); + } + + private static ObjectWrapper CreateUser(Engine engine, string id, bool isClient, string email, string? name, IEnumerable allClaims) + { + var claims = + allClaims.GroupBy(x => x.Type.Split(ClaimSeparators).Last()) + .ToDictionary( + x => x.Key, + x => x.Select(y => y.Value).ToArray()); + + return new ObjectWrapper(engine, new { id, isClient, email, name, claims }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs new file mode 100644 index 000000000..7d1d89193 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Security.Claims; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public sealed class ScriptContext + { + public ClaimsPrincipal User { get; set; } + + public Guid ContentId { get; set; } + + public NamedContentData? Data { get; set; } + + public NamedContentData DataOld { get; set; } + + public Status Status { get; set; } + + public Status StatusOld { get; set; } + + public string Operation { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj new file mode 100644 index 000000000..bdb436812 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -0,0 +1,33 @@ + + + netcoreapp3.0 + Squidex.Domain.Apps.Core + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs new file mode 100644 index 000000000..281893a1e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.Tags +{ + public interface ITagService + { + Task> GetTagIdsAsync(Guid appId, string group, HashSet names); + + Task> NormalizeTagsAsync(Guid appId, string group, HashSet? names, HashSet? ids); + + Task> DenormalizeTagsAsync(Guid appId, string group, HashSet ids); + + Task GetTagsAsync(Guid appId, string group); + + Task GetExportableTagsAsync(Guid appId, string group); + + Task RebuildTagsAsync(Guid appId, string group, TagsExport tags); + + Task ClearAsync(Guid appId, string group); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs new file mode 100644 index 000000000..8184d8e61 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs @@ -0,0 +1,150 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.Tags +{ + public static class TagNormalizer + { + public static async Task NormalizeAsync(this ITagService tagService, Guid appId, Guid schemaId, Schema schema, NamedContentData newData, NamedContentData? oldData) + { + Guard.NotNull(tagService); + Guard.NotNull(schema); + Guard.NotNull(newData); + + var newValues = new HashSet(); + var newArrays = new List(); + + var oldValues = new HashSet(); + var oldArrays = new List(); + + GetValues(schema, newValues, newArrays, newData); + + if (oldData != null) + { + GetValues(schema, oldValues, oldArrays, oldData); + } + + if (newValues.Count > 0) + { + var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), newValues, oldValues); + + foreach (var array in newArrays) + { + for (var i = 0; i < array.Count; i++) + { + if (normalized.TryGetValue(array[i].ToString(), out var result)) + { + array[i] = JsonValue.Create(result); + } + } + } + } + } + + public static async Task DenormalizeAsync(this ITagService tagService, Guid appId, Guid schemaId, Schema schema, params NamedContentData[] datas) + { + Guard.NotNull(tagService); + Guard.NotNull(schema); + + var tagsValues = new HashSet(); + var tagsArrays = new List(); + + GetValues(schema, tagsValues, tagsArrays, datas); + + if (tagsValues.Count > 0) + { + var denormalized = await tagService.DenormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), tagsValues); + + foreach (var array in tagsArrays) + { + for (var i = 0; i < array.Count; i++) + { + if (denormalized.TryGetValue(array[i].ToString(), out var result)) + { + array[i] = JsonValue.Create(result); + } + } + } + } + } + + private static void GetValues(Schema schema, HashSet values, List arrays, params NamedContentData[] datas) + { + foreach (var field in schema.Fields) + { + if (field is IField tags && tags.Properties.Normalization == TagsFieldNormalization.Schema) + { + foreach (var data in datas) + { + if (data.TryGetValue(field.Name, out var fieldData) && fieldData != null) + { + foreach (var partition in fieldData) + { + ExtractTags(partition.Value, values, arrays); + } + } + } + } + else if (field is IArrayField arrayField) + { + foreach (var nestedField in arrayField.Fields) + { + if (nestedField is IField nestedTags && nestedTags.Properties.Normalization == TagsFieldNormalization.Schema) + { + foreach (var data in datas) + { + if (data.TryGetValue(field.Name, out var fieldData) && fieldData != null) + { + foreach (var partition in fieldData) + { + if (partition.Value is JsonArray array) + { + foreach (var value in array) + { + if (value is JsonObject nestedObject) + { + if (nestedObject.TryGetValue(nestedField.Name, out var nestedValue)) + { + ExtractTags(nestedValue, values, arrays); + } + } + } + } + } + } + } + } + } + } + } + } + + private static void ExtractTags(IJsonValue value, ISet values, ICollection arrays) + { + if (value is JsonArray array) + { + foreach (var item in array) + { + if (item.Type == JsonValueType.String) + { + values.Add(item.ToString()); + } + } + + arrays.Add(array); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs new file mode 100644 index 000000000..1c1d209c2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Validation; + +#pragma warning disable SA1028, IDE0004 // Code must not contain trailing whitespace + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class ContentValidator + { + private readonly Schema schema; + private readonly PartitionResolver partitionResolver; + private readonly ValidationContext context; + private readonly ConcurrentBag errors = new ConcurrentBag(); + + public IReadOnlyCollection Errors + { + get { return errors; } + } + + public ContentValidator(Schema schema, PartitionResolver partitionResolver, ValidationContext context) + { + Guard.NotNull(schema); + Guard.NotNull(context); + Guard.NotNull(partitionResolver); + + this.schema = schema; + this.context = context; + this.partitionResolver = partitionResolver; + } + + private void AddError(IEnumerable path, string message) + { + var pathString = path.ToPathString(); + + errors.Add(new ValidationError(message, pathString)); + } + + public Task ValidatePartialAsync(NamedContentData data) + { + Guard.NotNull(data); + + var validator = CreateSchemaValidator(true); + + return validator.ValidateAsync(data, context, AddError); + } + + public Task ValidateAsync(NamedContentData data) + { + Guard.NotNull(data); + + var validator = CreateSchemaValidator(false); + + return validator.ValidateAsync(data, context, AddError); + } + + private IValidator CreateSchemaValidator(bool isPartial) + { + var fieldsValidators = new Dictionary(schema.Fields.Count); + + foreach (var field in schema.Fields) + { + fieldsValidators[field.Name] = (!field.RawProperties.IsRequired, CreateFieldValidator(field, isPartial)); + } + + return new ObjectValidator(fieldsValidators, isPartial, "field"); + } + + private IValidator CreateFieldValidator(IRootField field, bool isPartial) + { + var partitioning = partitionResolver(field.Partitioning); + + var fieldValidator = field.CreateValidator(); + var fieldsValidators = new Dictionary(); + + foreach (var partition in partitioning) + { + fieldsValidators[partition.Key] = (partition.IsOptional, fieldValidator); + } + + return new AggregateValidator( + field.CreateBagValidator() + .Union(Enumerable.Repeat( + new ObjectValidator(fieldsValidators, isPartial, TypeName(field)), 1))); + } + + private static string TypeName(IRootField field) + { + var isLanguage = field.Partitioning.Equals(Partitioning.Language); + + return isLanguage ? "language" : "invariant value"; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs new file mode 100644 index 000000000..7d8b842c9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class FieldBagValidatorsFactory : IFieldVisitor> + { + private static readonly FieldBagValidatorsFactory Instance = new FieldBagValidatorsFactory(); + + private FieldBagValidatorsFactory() + { + } + + public static IEnumerable CreateValidators(IField field) + { + Guard.NotNull(field); + + return field.Accept(Instance); + } + + public IEnumerable Visit(IArrayField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield return NoValueValidator.Instance; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs new file mode 100644 index 000000000..a5681d300 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs @@ -0,0 +1,191 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NodaTime; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class FieldValueValidatorsFactory : IFieldVisitor> + { + private static readonly FieldValueValidatorsFactory Instance = new FieldValueValidatorsFactory(); + + private FieldValueValidatorsFactory() + { + } + + public static IEnumerable CreateValidators(IField field) + { + Guard.NotNull(field); + + return field.Accept(Instance); + } + + public IEnumerable Visit(IArrayField field) + { + if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) + { + yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); + } + + var nestedSchema = new Dictionary(field.Fields.Count); + + foreach (var nestedField in field.Fields) + { + nestedSchema[nestedField.Name] = (false, nestedField.CreateValidator()); + } + + yield return new CollectionItemValidator(new ObjectValidator(nestedSchema, false, "field")); + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) + { + yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); + } + + if (!field.Properties.AllowDuplicates) + { + yield return new UniqueValuesValidator(); + } + + yield return new AssetsValidator(field.Properties); + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired) + { + yield return new RequiredValidator(); + } + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired) + { + yield return new RequiredValidator(); + } + + if (field.Properties.MinValue.HasValue || field.Properties.MaxValue.HasValue) + { + yield return new RangeValidator(field.Properties.MinValue, field.Properties.MaxValue); + } + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired) + { + yield return new RequiredValidator(); + } + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired) + { + yield return new RequiredValidator(); + } + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired) + { + yield return new RequiredValidator(); + } + + if (field.Properties.MinValue.HasValue || field.Properties.MaxValue.HasValue) + { + yield return new RangeValidator(field.Properties.MinValue, field.Properties.MaxValue); + } + + if (field.Properties.AllowedValues != null) + { + yield return new AllowedValuesValidator(field.Properties.AllowedValues); + } + + if (field.Properties.IsUnique) + { + yield return new UniqueValidator(); + } + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) + { + yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); + } + + if (!field.Properties.AllowDuplicates) + { + yield return new UniqueValuesValidator(); + } + + yield return new ReferencesValidator(field.Properties.SchemaIds); + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired) + { + yield return new RequiredStringValidator(true); + } + + if (field.Properties.MinLength.HasValue || field.Properties.MaxLength.HasValue) + { + yield return new StringLengthValidator(field.Properties.MinLength, field.Properties.MaxLength); + } + + if (!string.IsNullOrWhiteSpace(field.Properties.Pattern)) + { + yield return new PatternValidator(field.Properties.Pattern, field.Properties.PatternMessage); + } + + if (field.Properties.AllowedValues != null) + { + yield return new AllowedValuesValidator(field.Properties.AllowedValues); + } + + if (field.Properties.IsUnique) + { + yield return new UniqueValidator(); + } + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) + { + yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); + } + + if (field.Properties.AllowedValues != null) + { + yield return new CollectionItemValidator(new AllowedValuesValidator(field.Properties.AllowedValues)); + } + + yield return new CollectionItemValidator(new RequiredStringValidator(true)); + } + + public IEnumerable Visit(IField field) + { + if (field is INestedField) + { + yield return NoValueValidator.Instance; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs new file mode 100644 index 000000000..d1ffd0067 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs @@ -0,0 +1,231 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NodaTime.Text; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class JsonValueConverter : IFieldVisitor + { + private readonly IJsonValue value; + + private JsonValueConverter(IJsonValue value) + { + this.value = value; + } + + public static object ConvertValue(IField field, IJsonValue json) + { + return field.Accept(new JsonValueConverter(json)); + } + + public object Visit(IArrayField field) + { + return ConvertToObjectList(); + } + + public object Visit(IField field) + { + return ConvertToGuidList(); + } + + public object Visit(IField field) + { + return ConvertToGuidList(); + } + + public object Visit(IField field) + { + return ConvertToStringList(); + } + + public object Visit(IField field) + { + if (value is JsonScalar b) + { + return b.Value; + } + + throw new InvalidCastException("Invalid json type, expected boolean."); + } + + public object Visit(IField field) + { + if (value is JsonScalar b) + { + return b.Value; + } + + throw new InvalidCastException("Invalid json type, expected number."); + } + + public object Visit(IField field) + { + if (value is JsonScalar b) + { + return b.Value; + } + + throw new InvalidCastException("Invalid json type, expected string."); + } + + public object Visit(IField field) + { + return value; + } + + public object Visit(IField field) + { + if (value.Type == JsonValueType.String) + { + var parseResult = InstantPattern.General.Parse(value.ToString()); + + if (!parseResult.Success) + { + throw parseResult.Exception; + } + + return parseResult.Value; + } + + throw new InvalidCastException("Invalid json type, expected string."); + } + + public object Visit(IField field) + { + if (value is JsonObject geolocation) + { + foreach (var propertyName in geolocation.Keys) + { + if (!string.Equals(propertyName, "latitude", StringComparison.OrdinalIgnoreCase) && + !string.Equals(propertyName, "longitude", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidCastException("Geolocation can only have latitude and longitude property."); + } + } + + if (geolocation.TryGetValue("latitude", out var latValue) && latValue is JsonScalar latNumber) + { + var lat = latNumber.Value; + + if (!lat.IsBetween(-90, 90)) + { + throw new InvalidCastException("Latitude must be between -90 and 90."); + } + } + else + { + throw new InvalidCastException("Invalid json type, expected latitude/longitude object."); + } + + if (geolocation.TryGetValue("longitude", out var lonValue) && lonValue is JsonScalar lonNumber) + { + var lon = lonNumber.Value; + + if (!lon.IsBetween(-180, 180)) + { + throw new InvalidCastException("Longitude must be between -180 and 180."); + } + } + else + { + throw new InvalidCastException("Invalid json type, expected latitude/longitude object."); + } + + return value; + } + + throw new InvalidCastException("Invalid json type, expected latitude/longitude object."); + } + + public object Visit(IField field) + { + return value; + } + + private object ConvertToGuidList() + { + if (value is JsonArray array) + { + var result = new List(); + + foreach (var item in array) + { + if (item is JsonScalar s && Guid.TryParse(s.Value, out var guid)) + { + result.Add(guid); + } + else + { + throw new InvalidCastException("Invalid json type, expected array of guid strings."); + } + } + + return result; + } + + throw new InvalidCastException("Invalid json type, expected array of guid strings."); + } + + private object ConvertToStringList() + { + if (value is JsonArray array) + { + var result = new List(); + + foreach (var item in array) + { + if (item is JsonNull) + { + result.Add(null); + } + else if (item is JsonScalar s) + { + result.Add(s.Value); + } + else + { + throw new InvalidCastException("Invalid json type, expected array of strings."); + } + } + + return result; + } + + throw new InvalidCastException("Invalid json type, expected array of strings."); + } + + private object ConvertToObjectList() + { + if (value is JsonArray array) + { + var result = new List(); + + foreach (var item in array) + { + if (item is JsonObject obj) + { + result.Add(obj); + } + else + { + throw new InvalidCastException("Invalid json type, expected array of objects."); + } + } + + return result; + } + + throw new InvalidCastException("Invalid json type, expected array of objects."); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs new file mode 100644 index 000000000..0962a5e4b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public static class Undefined + { + public static readonly object Value = new object(); + + public static bool IsUndefined(this object? other) + { + return ReferenceEquals(other, Value); + } + + public static bool IsNullOrUndefined(this object? other) + { + return other == null || other.IsUndefined(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs new file mode 100644 index 000000000..3bddf058d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs @@ -0,0 +1,127 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public delegate Task> CheckContents(Guid schemaId, FilterNode filter); + + public delegate Task> CheckContentsByIds(HashSet ids); + + public delegate Task> CheckAssets(IEnumerable ids); + + public sealed class ValidationContext + { + private readonly Guid contentId; + private readonly Guid schemaId; + private readonly CheckContents checkContent; + private readonly CheckContentsByIds checkContentByIds; + private readonly CheckAssets checkAsset; + private readonly ImmutableQueue propertyPath; + + public ImmutableQueue Path + { + get { return propertyPath; } + } + + public Guid ContentId + { + get { return contentId; } + } + + public Guid SchemaId + { + get { return schemaId; } + } + + public bool IsOptional { get; } + + public ValidationContext( + Guid contentId, + Guid schemaId, + CheckContents checkContent, + CheckContentsByIds checkContentsByIds, + CheckAssets checkAsset) + : this(contentId, schemaId, checkContent, checkContentsByIds, checkAsset, ImmutableQueue.Empty, false) + { + } + + private ValidationContext( + Guid contentId, + Guid schemaId, + CheckContents checkContent, + CheckContentsByIds checkContentByIds, + CheckAssets checkAsset, + ImmutableQueue propertyPath, + bool isOptional) + { + Guard.NotNull(checkAsset); + Guard.NotNull(checkContent); + Guard.NotNull(checkContentByIds); + + this.propertyPath = propertyPath; + + this.checkContent = checkContent; + this.checkContentByIds = checkContentByIds; + this.checkAsset = checkAsset; + this.contentId = contentId; + + this.schemaId = schemaId; + + IsOptional = isOptional; + } + + public ValidationContext Optional(bool isOptional) + { + return isOptional == IsOptional ? this : OptionalCore(isOptional); + } + + private ValidationContext OptionalCore(bool isOptional) + { + return new ValidationContext( + contentId, + schemaId, + checkContent, + checkContentByIds, + checkAsset, + propertyPath, + isOptional); + } + + public ValidationContext Nested(string property) + { + return new ValidationContext( + contentId, schemaId, + checkContent, + checkContentByIds, + checkAsset, + propertyPath.Enqueue(property), + IsOptional); + } + + public Task> GetContentIdsAsync(HashSet ids) + { + return checkContentByIds(ids); + } + + public Task> GetContentIdsAsync(Guid schemaId, FilterNode filter) + { + return checkContent(schemaId, filter); + } + + public Task> GetAssetInfosAsync(IEnumerable assetId) + { + return checkAsset(assetId); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs new file mode 100644 index 000000000..2f79b2ba6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class AggregateValidator : IValidator + { + private readonly IValidator[]? validators; + + public AggregateValidator(IEnumerable? validators) + { + this.validators = validators?.ToArray(); + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (validators?.Length > 0) + { + return Task.WhenAll(validators.Select(x => x.ValidateAsync(value, context, addError))); + } + + return Task.CompletedTask; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs new file mode 100644 index 000000000..680ee979c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class AllowedValuesValidator : IValidator + { + private readonly IEnumerable allowedValues; + + public AllowedValuesValidator(params T[] allowedValues) + : this((IEnumerable)allowedValues) + { + } + + public AllowedValuesValidator(IEnumerable allowedValues) + { + Guard.NotNull(allowedValues); + + this.allowedValues = allowedValues; + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value != null && value is T typedValue && !allowedValues.Contains(typedValue)) + { + addError(context.Path, "Not an allowed value."); + } + + return TaskHelper.Done; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs new file mode 100644 index 000000000..a991b4e18 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class AssetsValidator : IValidator + { + private readonly AssetsFieldProperties properties; + + public AssetsValidator(AssetsFieldProperties properties) + { + this.properties = properties; + } + + public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value is ICollection assetIds && assetIds.Count > 0) + { + var assets = await context.GetAssetInfosAsync(assetIds); + var index = 0; + + foreach (var assetId in assetIds) + { + index++; + + var path = context.Path.Enqueue($"[{index}]"); + + var asset = assets.FirstOrDefault(x => x.AssetId == assetId); + + if (asset == null) + { + addError(path, $"Id '{assetId}' not found."); + continue; + } + + if (properties.MinSize.HasValue && asset.FileSize < properties.MinSize) + { + addError(path, $"'{asset.FileSize.ToReadableSize()}' less than minimum of '{properties.MinSize.Value.ToReadableSize()}'."); + } + + if (properties.MaxSize.HasValue && asset.FileSize > properties.MaxSize) + { + addError(path, $"'{asset.FileSize.ToReadableSize()}' greater than maximum of '{properties.MaxSize.Value.ToReadableSize()}'."); + } + + if (properties.AllowedExtensions != null && + properties.AllowedExtensions.Count > 0 && + !properties.AllowedExtensions.Any(x => asset.FileName.EndsWith("." + x, StringComparison.OrdinalIgnoreCase))) + { + addError(path, "Invalid file extension."); + } + + if (!asset.IsImage) + { + if (properties.MustBeImage) + { + addError(path, "Not an image."); + } + + continue; + } + + if (asset.PixelWidth.HasValue && + asset.PixelHeight.HasValue) + { + var w = asset.PixelWidth.Value; + var h = asset.PixelHeight.Value; + + var actualRatio = (double)w / h; + + if (properties.MinWidth.HasValue && w < properties.MinWidth) + { + addError(path, $"Width '{w}px' less than minimum of '{properties.MinWidth}px'."); + } + + if (properties.MaxWidth.HasValue && w > properties.MaxWidth) + { + addError(path, $"Width '{w}px' greater than maximum of '{properties.MaxWidth}px'."); + } + + if (properties.MinHeight.HasValue && h < properties.MinHeight) + { + addError(path, $"Height '{h}px' less than minimum of '{properties.MinHeight}px'."); + } + + if (properties.MaxHeight.HasValue && h > properties.MaxHeight) + { + addError(path, $"Height '{h}px' greater than maximum of '{properties.MaxHeight}px'."); + } + + if (properties.AspectHeight.HasValue && properties.AspectWidth.HasValue) + { + var expectedRatio = (double)properties.AspectWidth.Value / properties.AspectHeight.Value; + + if (Math.Abs(expectedRatio - actualRatio) > double.Epsilon) + { + addError(path, $"Aspect ratio not '{properties.AspectWidth}:{properties.AspectHeight}'."); + } + } + } + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs new file mode 100644 index 000000000..6fd866f2b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class CollectionItemValidator : IValidator + { + private readonly IValidator[] itemValidators; + + public CollectionItemValidator(params IValidator[] itemValidators) + { + Guard.NotNull(itemValidators); + Guard.NotEmpty(itemValidators); + + this.itemValidators = itemValidators; + } + + public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value is ICollection items && items.Count > 0) + { + var innerTasks = new List(); + var index = 1; + + foreach (var item in items) + { + var innerContext = context.Nested($"[{index}]"); + + foreach (var itemValidator in itemValidators) + { + innerTasks.Add(itemValidator.ValidateAsync(item, innerContext, addError)); + } + + index++; + } + + await Task.WhenAll(innerTasks); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs new file mode 100644 index 000000000..65ddf8311 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class CollectionValidator : IValidator + { + private readonly bool isRequired; + private readonly int? minItems; + private readonly int? maxItems; + + public CollectionValidator(bool isRequired, int? minItems = null, int? maxItems = null) + { + if (minItems.HasValue && maxItems.HasValue && minItems.Value > maxItems.Value) + { + throw new ArgumentException("Min length must be greater than max length.", nameof(minItems)); + } + + this.isRequired = isRequired; + this.minItems = minItems; + this.maxItems = maxItems; + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (!(value is ICollection items) || items.Count == 0) + { + if (isRequired && !context.IsOptional) + { + addError(context.Path, "Field is required."); + } + + return TaskHelper.Done; + } + + if (minItems.HasValue && maxItems.HasValue) + { + if (minItems == maxItems && minItems != items.Count) + { + addError(context.Path, $"Must have exactly {maxItems} item(s)."); + } + else if (items.Count < minItems || items.Count > maxItems) + { + addError(context.Path, $"Must have between {minItems} and {maxItems} item(s)."); + } + } + else + { + if (minItems.HasValue && items.Count < minItems.Value) + { + addError(context.Path, $"Must have at least {minItems} item(s)."); + } + + if (maxItems.HasValue && items.Count > maxItems.Value) + { + addError(context.Path, $"Must not have more than {maxItems} item(s)."); + } + } + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs new file mode 100644 index 000000000..7e306bcde --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class FieldValidator : IValidator + { + private readonly IValidator[] validators; + private readonly IField field; + + public FieldValidator(IEnumerable validators, IField field) + { + Guard.NotNull(field); + + this.validators = validators.ToArray(); + + this.field = field; + } + + public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + try + { + object? typedValue = value; + + if (value is IJsonValue jsonValue) + { + if (jsonValue.Type == JsonValueType.Null) + { + typedValue = null; + } + else + { + typedValue = JsonValueConverter.ConvertValue(field, jsonValue); + } + } + + if (validators?.Length > 0) + { + var tasks = new List(); + + foreach (var validator in validators) + { + tasks.Add(validator.ValidateAsync(typedValue, context, addError)); + } + + await Task.WhenAll(tasks); + } + } + catch + { + addError(context.Path, "Not a valid value."); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs new file mode 100644 index 000000000..fbe2a92f4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public delegate void AddError(IEnumerable path, string message); + + public interface IValidator + { + Task ValidateAsync(object? value, ValidationContext context, AddError addError); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs new file mode 100644 index 000000000..6e0907836 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class NoValueValidator : IValidator + { + public static readonly NoValueValidator Instance = new NoValueValidator(); + + private NoValueValidator() + { + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (!value.IsUndefined()) + { + addError(context.Path, "Value must not be defined."); + } + + return Task.CompletedTask; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs new file mode 100644 index 000000000..e2b8058df --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class ObjectValidator : IValidator + { + private static readonly IReadOnlyDictionary DefaultValue = new Dictionary(); + private readonly IDictionary schema; + private readonly bool isPartial; + private readonly string fieldType; + + public ObjectValidator(IDictionary schema, bool isPartial, string fieldType) + { + this.schema = schema; + this.fieldType = fieldType; + this.isPartial = isPartial; + } + + public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value.IsNullOrUndefined()) + { + value = DefaultValue; + } + + if (value is IReadOnlyDictionary values) + { + foreach (var fieldData in values) + { + var name = fieldData.Key; + + if (!schema.ContainsKey(name)) + { + addError(context.Path.Enqueue(name), $"Not a known {fieldType}."); + } + } + + var tasks = new List(); + + foreach (var field in schema) + { + var name = field.Key; + + var (isOptional, validator) = field.Value; + + object? fieldValue = Undefined.Value; + + if (!values.TryGetValue(name, out var temp)) + { + if (isPartial) + { + continue; + } + } + else + { + fieldValue = temp; + } + + var fieldContext = context.Nested(name).Optional(isOptional); + + tasks.Add(validator.ValidateAsync(fieldValue, fieldContext, addError)); + } + + await Task.WhenAll(tasks); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs new file mode 100644 index 000000000..97d9cf06d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class PatternValidator : IValidator + { + private static readonly TimeSpan Timeout = TimeSpan.FromMilliseconds(20); + private readonly Regex regex; + private readonly string? errorMessage; + + public PatternValidator(string pattern, string? errorMessage = null) + { + this.errorMessage = errorMessage; + + regex = new Regex($"^{pattern}$", RegexOptions.None, Timeout); + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value is string stringValue) + { + if (!string.IsNullOrEmpty(stringValue)) + { + try + { + if (!regex.IsMatch(stringValue)) + { + if (string.IsNullOrWhiteSpace(errorMessage)) + { + addError(context.Path, "Does not match to the pattern."); + } + else + { + addError(context.Path, errorMessage); + } + } + } + catch + { + addError(context.Path, "Regex is too slow."); + } + } + } + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs new file mode 100644 index 000000000..a1133f0c5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class RangeValidator : IValidator where T : struct, IComparable + { + private readonly T? min; + private readonly T? max; + + public RangeValidator(T? min, T? max) + { + if (min.HasValue && max.HasValue && min.Value.CompareTo(max.Value) > 0) + { + throw new ArgumentException("Min value must be greater than max value.", nameof(min)); + } + + this.min = min; + this.max = max; + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value != null && value is T typedValue) + { + if (min.HasValue && max.HasValue) + { + if (Equals(min, max) && Equals(min.Value, max.Value)) + { + addError(context.Path, $"Must be exactly '{max}'."); + } + else if (typedValue.CompareTo(min.Value) < 0 || typedValue.CompareTo(max.Value) > 0) + { + addError(context.Path, $"Must be between '{min}' and '{max}'."); + } + } + else + { + if (min.HasValue && typedValue.CompareTo(min.Value) < 0) + { + addError(context.Path, $"Must be greater or equal to '{min}'."); + } + + if (max.HasValue && typedValue.CompareTo(max.Value) > 0) + { + addError(context.Path, $"Must be less or equal to '{max}'."); + } + } + } + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs new file mode 100644 index 000000000..09815efd4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class ReferencesValidator : IValidator + { + private readonly IEnumerable? schemaIds; + + public ReferencesValidator(IEnumerable? schemaIds) + { + this.schemaIds = schemaIds; + } + + public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value is ICollection contentIds) + { + var foundIds = await context.GetContentIdsAsync(contentIds.ToHashSet()); + + foreach (var id in contentIds) + { + var (schemaId, _) = foundIds.FirstOrDefault(x => x.Id == id); + + if (schemaId == Guid.Empty) + { + addError(context.Path, $"Contains invalid reference '{id}'."); + } + else if (schemaIds?.Any() == true && !schemaIds.Contains(schemaId)) + { + addError(context.Path, $"Contains reference '{id}' to invalid schema."); + } + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs new file mode 100644 index 000000000..97e411a09 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class RequiredStringValidator : IValidator + { + private readonly bool validateEmptyStrings; + + public RequiredStringValidator(bool validateEmptyStrings = false) + { + this.validateEmptyStrings = validateEmptyStrings; + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (context.IsOptional) + { + return TaskHelper.Done; + } + + if (value.IsNullOrUndefined() || IsEmptyString(value)) + { + addError(context.Path, "Field is required."); + } + + return TaskHelper.Done; + } + + private bool IsEmptyString(object? value) + { + return value is string typed && validateEmptyStrings && string.IsNullOrWhiteSpace(typed); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs new file mode 100644 index 000000000..ef9cf1027 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class RequiredValidator : IValidator + { + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value.IsNullOrUndefined() && !context.IsOptional) + { + addError(context.Path, "Field is required."); + } + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs new file mode 100644 index 000000000..08be9d0b0 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class StringLengthValidator : IValidator + { + private readonly int? minLength; + private readonly int? maxLength; + + public StringLengthValidator(int? minLength, int? maxLength) + { + if (minLength.HasValue && maxLength.HasValue && minLength.Value > maxLength.Value) + { + throw new ArgumentException("Min length must be greater than max length.", nameof(minLength)); + } + + this.minLength = minLength; + this.maxLength = maxLength; + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) + { + if (minLength.HasValue && maxLength.HasValue) + { + if (minLength == maxLength && minLength != stringValue.Length) + { + addError(context.Path, $"Must have exactly {maxLength} character(s)."); + } + else if (stringValue.Length < minLength || stringValue.Length > maxLength) + { + addError(context.Path, $"Must have between {minLength} and {maxLength} character(s)."); + } + } + else + { + if (minLength.HasValue && stringValue.Length < minLength.Value) + { + addError(context.Path, $"Must have at least {minLength} character(s)."); + } + + if (maxLength.HasValue && stringValue.Length > maxLength.Value) + { + addError(context.Path, $"Must not have more than {maxLength} character(s)."); + } + } + } + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs new file mode 100644 index 000000000..fee6cf8e7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class UniqueValidator : IValidator + { + public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + var count = context.Path.Count(); + + if (value != null && (count == 0 || (count == 2 && context.Path.Last() == InvariantPartitioning.Key))) + { + FilterNode? filter = null; + + if (value is string s) + { + filter = ClrFilter.Eq(Path(context), s); + } + else if (value is double d) + { + filter = ClrFilter.Eq(Path(context), d); + } + + if (filter != null) + { + var found = await context.GetContentIdsAsync(context.SchemaId, filter); + + if (found.Any(x => x.Id != context.ContentId)) + { + addError(context.Path, "Another content with the same value exists."); + } + } + } + } + + private static List Path(ValidationContext context) + { + return Enumerable.Repeat("Data", 1).Union(context.Path).ToList(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs new file mode 100644 index 000000000..c9e9f724f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class UniqueValuesValidator : IValidator + { + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value is IEnumerable items && items.Any()) + { + var itemsArray = items.ToArray(); + + if (itemsArray.Length != itemsArray.Distinct().Count()) + { + addError(context.Path, "Must not contain duplicate values."); + } + } + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs new file mode 100644 index 000000000..536eaee23 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -0,0 +1,148 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +{ + public sealed partial class MongoAssetRepository : MongoRepositoryBase, IAssetRepository + { + public MongoAssetRepository(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "States_Assets"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel( + Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.Tags) + .Descending(x => x.LastModified)), + new CreateIndexModel( + Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.Slug)) + }, ct); + } + + public async Task> QueryAsync(Guid appId, ClrQuery query) + { + using (Profiler.TraceMethod("QueryAsyncByQuery")) + { + try + { + query = query.AdjustToModel(); + + var filter = query.BuildFilter(appId); + + var contentCount = Collection.Find(filter).CountDocumentsAsync(); + var contentItems = + Collection.Find(filter) + .AssetTake(query) + .AssetSkip(query) + .AssetSort(query) + .ToListAsync(); + + await Task.WhenAll(contentItems, contentCount); + + return ResultList.Create(contentCount.Result, contentItems.Result); + } + catch (MongoQueryException ex) + { + if (ex.Message.Contains("17406")) + { + throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items."); + } + else + { + throw; + } + } + } + } + + public async Task> QueryAsync(Guid appId, HashSet ids) + { + using (Profiler.TraceMethod("QueryAsyncByIds")) + { + var find = Collection.Find(x => ids.Contains(x.Id)).SortByDescending(x => x.LastModified); + + var assetItems = await find.ToListAsync(); + + return ResultList.Create(assetItems.Count, assetItems.OfType()); + } + } + + public async Task FindAssetBySlugAsync(Guid appId, string slug) + { + using (Profiler.TraceMethod()) + { + var assetEntity = + await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.Slug == slug) + .FirstOrDefaultAsync(); + + return assetEntity; + } + } + + public async Task> QueryByHashAsync(Guid appId, string hash) + { + using (Profiler.TraceMethod()) + { + var assetEntities = + await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.FileHash == hash) + .ToListAsync(); + + return assetEntities.OfType().ToList(); + } + } + + public async Task FindAssetAsync(Guid id, bool allowDeleted = false) + { + using (Profiler.TraceMethod()) + { + var assetEntity = + await Collection.Find(x => x.Id == id) + .FirstOrDefaultAsync(); + + if (assetEntity?.IsDeleted == true && !allowDeleted) + { + return null; + } + + return assetEntity; + } + } + + public Task RemoveAsync(Guid key) + { + return Collection.DeleteOneAsync(x => x.Id == key); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs new file mode 100644 index 000000000..c50f90efd --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +{ + public sealed partial class MongoAssetRepository : ISnapshotStore + { + async Task<(AssetState Value, long Version)> ISnapshotStore.ReadAsync(Guid key) + { + using (Profiler.TraceMethod()) + { + var existing = + await Collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (Map(existing), existing.Version); + } + + return (null!, EtagVersion.NotFound); + } + } + + async Task ISnapshotStore.WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion) + { + using (Profiler.TraceMethod()) + { + var entity = SimpleMapper.Map(value, new MongoAssetEntity()); + + entity.Version = newVersion; + entity.IndexedAppId = value.AppId.Id; + + await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, entity, Upsert); + } + } + + async Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct) + { + using (Profiler.TraceMethod()) + { + await Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(x => callback(Map(x), x.Version), ct); + } + } + + async Task ISnapshotStore.RemoveAsync(Guid key) + { + using (Profiler.TraceMethod()) + { + await Collection.DeleteOneAsync(x => x.Id == key); + } + } + + private static AssetState Map(MongoAssetEntity existing) + { + return SimpleMapper.Map(existing, new AssetState()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs new file mode 100644 index 000000000..9485857b9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -0,0 +1,271 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents +{ + internal class MongoContentCollection : MongoRepositoryBase + { + private readonly IAppProvider appProvider; + private readonly IJsonSerializer serializer; + + public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider) + : base(database) + { + this.appProvider = appProvider; + + this.serializer = serializer; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel(Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.Status) + .Ascending(x => x.Id)), + new CreateIndexModel(Index + .Ascending(x => x.IndexedSchemaId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.Status) + .Ascending(x => x.Id)), + new CreateIndexModel(Index + .Ascending(x => x.ScheduledAt) + .Ascending(x => x.IsDeleted)), + new CreateIndexModel(Index + .Ascending(x => x.ReferencedIds)) + }, ct); + } + + protected override string CollectionName() + { + return "State_Contents"; + } + + public async Task> QueryAsync(ISchemaEntity schema, ClrQuery query, List? ids, Status[]? status, bool inDraft, bool includeDraft = true) + { + try + { + query = query.AdjustToModel(schema.SchemaDef, inDraft); + + var filter = query.ToFilter(schema.Id, ids, status); + + var contentCount = Collection.Find(filter).CountDocumentsAsync(); + var contentItems = + Collection.Find(filter) + .WithoutDraft(includeDraft) + .ContentTake(query) + .ContentSkip(query) + .ContentSort(query) + .ToListAsync(); + + await Task.WhenAll(contentItems, contentCount); + + foreach (var entity in contentItems.Result) + { + entity.ParseData(schema.SchemaDef, serializer); + } + + return ResultList.Create(contentCount.Result, contentItems.Result); + } + catch (MongoQueryException ex) + { + if (ex.Message.Contains("17406")) + { + throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items."); + } + else + { + throw; + } + } + } + + public async Task> QueryAsync(IAppEntity app, HashSet ids, Status[]? status, bool includeDraft) + { + var find = Collection.Find(FilterFactory.IdsByApp(app.Id, ids, status)); + + var contentItems = await find.WithoutDraft(includeDraft).ToListAsync(); + + var schemaIds = contentItems.Select(x => x.IndexedSchemaId).ToList(); + var schemas = await Task.WhenAll(schemaIds.Select(x => appProvider.GetSchemaAsync(app.Id, x))); + + var result = new List<(IContentEntity Content, ISchemaEntity Schema)>(); + + foreach (var entity in contentItems) + { + var schema = schemas.FirstOrDefault(x => x.Id == entity.IndexedSchemaId); + + if (schema != null) + { + entity.ParseData(schema.SchemaDef, serializer); + + result.Add((entity, schema)); + } + } + + return result; + } + + public async Task> QueryAsync(ISchemaEntity schema, HashSet ids, Status[]? status, bool includeDraft) + { + var find = Collection.Find(FilterFactory.IdsBySchema(schema.Id, ids, status)); + + var contentItems = await find.WithoutDraft(includeDraft).ToListAsync(); + + foreach (var entity in contentItems) + { + entity.ParseData(schema.SchemaDef, serializer); + } + + return ResultList.Create(contentItems.Count, contentItems); + } + + public async Task FindContentAsync(ISchemaEntity schema, Guid id, Status[]? status, bool includeDraft) + { + var find = Collection.Find(FilterFactory.Build(schema.Id, id, status)); + + var contentEntity = await find.WithoutDraft(includeDraft).FirstOrDefaultAsync(); + + contentEntity?.ParseData(schema.SchemaDef, serializer); + + return contentEntity; + } + + public Task QueryScheduledWithoutDataAsync(Instant now, Func callback) + { + return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true) + .Not(x => x.DataByIds) + .Not(x => x.DataDraftByIds) + .ForEachAsync(c => + { + callback(c); + }); + } + + public async Task> QueryIdsAsync(ISchemaEntity schema, FilterNode filterNode) + { + var filter = filterNode.AdjustToModel(schema.SchemaDef, true)?.ToFilter(schema.Id); + + var contentEntities = + await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId) + .ToListAsync(); + + return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList(); + } + + public async Task> QueryIdsAsync(HashSet ids) + { + var contentEntities = + await Collection.Find(Filter.In(x => x.Id, ids)).Only(x => x.Id, x => x.IndexedSchemaId) + .ToListAsync(); + + return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList(); + } + + public async Task> QueryIdsAsync(Guid appId) + { + var contentEntities = + await Collection.Find(x => x.IndexedAppId == appId).Only(x => x.Id) + .ToListAsync(); + + return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); + } + + public async Task<(ContentState Value, long Version)> ReadAsync(Guid key, Func> getSchema) + { + var contentEntity = + await Collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (contentEntity != null) + { + var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); + + contentEntity.ParseData(schema.SchemaDef, serializer); + + return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); + } + + return (null!, EtagVersion.NotFound); + } + + public Task ReadAllAsync(Func callback, Func> getSchema, CancellationToken ct = default) + { + return Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(async contentEntity => + { + var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); + + contentEntity.ParseData(schema.SchemaDef, serializer); + + await callback(SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); + }, ct); + } + + public Task CleanupAsync(Guid id) + { + return Collection.UpdateManyAsync( + Filter.And( + Filter.AnyEq(x => x.ReferencedIds, id), + Filter.AnyNe(x => x.ReferencedIdsDeleted, id)), + Update.AddToSet(x => x.ReferencedIdsDeleted, id)); + } + + public Task RemoveAsync(Guid id) + { + return Collection.DeleteOneAsync(x => x.Id == id); + } + + public async Task UpsertAsync(MongoContentEntity content, long oldVersion) + { + try + { + await Collection.ReplaceOneAsync(x => x.Id == content.Id && x.Version == oldVersion, content, Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await Collection.Find(x => x.Id == content.Id).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion["vs"].AsInt64, oldVersion, ex); + } + } + else + { + throw; + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs new file mode 100644 index 000000000..96e991253 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -0,0 +1,133 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents +{ + public sealed class MongoContentEntity : IContentEntity + { + private NamedContentData? data; + private NamedContentData dataDraft; + + [BsonId] + [BsonElement("_id")] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + + [BsonRequired] + [BsonElement("_ai")] + [BsonRepresentation(BsonType.String)] + public Guid IndexedAppId { get; set; } + + [BsonRequired] + [BsonElement("_si")] + [BsonRepresentation(BsonType.String)] + public Guid IndexedSchemaId { get; set; } + + [BsonRequired] + [BsonElement("rf")] + [BsonRepresentation(BsonType.String)] + public List? ReferencedIds { get; set; } + + [BsonRequired] + [BsonElement("rd")] + [BsonRepresentation(BsonType.String)] + public List ReferencedIdsDeleted { get; set; } = new List(); + + [BsonRequired] + [BsonElement("ss")] + public Status Status { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("do")] + [BsonJson] + public IdContentData DataByIds { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("dd")] + [BsonJson] + public IdContentData DataDraftByIds { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("sj")] + [BsonJson] + public ScheduleJob? ScheduleJob { get; set; } + + [BsonRequired] + [BsonElement("ai")] + public NamedId AppId { get; set; } + + [BsonRequired] + [BsonElement("si")] + public NamedId SchemaId { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("sa")] + public Instant? ScheduledAt { get; set; } + + [BsonRequired] + [BsonElement("ct")] + public Instant Created { get; set; } + + [BsonRequired] + [BsonElement("mt")] + public Instant LastModified { get; set; } + + [BsonRequired] + [BsonElement("vs")] + public long Version { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement("dl")] + public bool IsDeleted { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement("pd")] + public bool IsPending { get; set; } + + [BsonRequired] + [BsonElement("cb")] + public RefToken CreatedBy { get; set; } + + [BsonRequired] + [BsonElement("mb")] + public RefToken LastModifiedBy { get; set; } + + [BsonIgnore] + public NamedContentData? Data + { + get { return data; } + } + + [BsonIgnore] + public NamedContentData DataDraft + { + get { return dataDraft; } + } + + public void ParseData(Schema schema, IJsonSerializer serializer) + { + data = DataByIds?.FromMongoModel(schema, ReferencedIdsDeleted, serializer); + + if (DataDraftByIds != null) + { + dataDraft = DataDraftByIds.FromMongoModel(schema, ReferencedIdsDeleted, serializer); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs new file mode 100644 index 000000000..402b89427 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -0,0 +1,156 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Text; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents +{ + public partial class MongoContentRepository : IContentRepository, IInitializable + { + private static readonly List<(Guid SchemaId, Guid Id)> EmptyIds = new List<(Guid SchemaId, Guid Id)>(); + private readonly IAppProvider appProvider; + private readonly IJsonSerializer serializer; + private readonly ITextIndexer indexer; + private readonly string typeAssetDeleted; + private readonly string typeContentDeleted; + private readonly MongoContentCollection contents; + + static MongoContentRepository() + { + StatusSerializer.Register(); + } + + public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer, ITextIndexer indexer, TypeNameRegistry typeNameRegistry) + { + Guard.NotNull(appProvider); + Guard.NotNull(serializer); + Guard.NotNull(indexer); + Guard.NotNull(typeNameRegistry); + + this.appProvider = appProvider; + this.indexer = indexer; + this.serializer = serializer; + + typeAssetDeleted = typeNameRegistry.GetName(); + typeContentDeleted = typeNameRegistry.GetName(); + + contents = new MongoContentCollection(database, serializer, appProvider); + } + + public Task InitializeAsync(CancellationToken ct = default) + { + return contents.InitializeAsync(ct); + } + + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, bool inDraft, ClrQuery query, bool includeDraft = true) + { + Guard.NotNull(app); + Guard.NotNull(schema); + Guard.NotNull(query); + + using (Profiler.TraceMethod("QueryAsyncByQuery")) + { + var fullTextIds = await indexer.SearchAsync(query.FullText, app, schema.Id, inDraft ? Scope.Draft : Scope.Published); + + if (fullTextIds?.Count == 0) + { + return ResultList.CreateFrom(0); + } + + return await contents.QueryAsync(schema, query, fullTextIds, status, inDraft, includeDraft); + } + } + + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, HashSet ids, bool includeDraft = true) + { + Guard.NotNull(app); + Guard.NotNull(ids); + Guard.NotNull(schema); + + using (Profiler.TraceMethod("QueryAsyncByIds")) + { + return await contents.QueryAsync(schema, ids, status, includeDraft); + } + } + + public async Task> QueryAsync(IAppEntity app, Status[]? status, HashSet ids, bool includeDraft = true) + { + Guard.NotNull(app); + Guard.NotNull(ids); + + using (Profiler.TraceMethod("QueryAsyncByIdsWithoutSchema")) + { + return await contents.QueryAsync(app, ids, status, includeDraft); + } + } + + public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, Guid id, bool includeDraft = true) + { + Guard.NotNull(app); + Guard.NotNull(schema); + + using (Profiler.TraceMethod()) + { + return await contents.FindContentAsync(schema, id, status, includeDraft); + } + } + + public async Task QueryScheduledWithoutDataAsync(Instant now, Func callback) + { + using (Profiler.TraceMethod()) + { + await contents.QueryScheduledWithoutDataAsync(now, callback); + } + } + + public async Task> QueryIdsAsync(Guid appId, HashSet ids) + { + using (Profiler.TraceMethod()) + { + return await contents.QueryIdsAsync(ids); + } + } + + public async Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode) + { + using (Profiler.TraceMethod()) + { + var schema = await appProvider.GetSchemaAsync(appId, schemaId); + + if (schema == null) + { + return EmptyIds; + } + + return await contents.QueryIdsAsync(schema, filterNode); + } + } + + public Task ClearAsync() + { + return contents.ClearAsync(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs new file mode 100644 index 000000000..c9196b081 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -0,0 +1,93 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents +{ + public partial class MongoContentRepository : ISnapshotStore + { + async Task ISnapshotStore.RemoveAsync(Guid key) + { + using (Profiler.TraceMethod()) + { + await contents.RemoveAsync(key); + } + } + + async Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct) + { + using (Profiler.TraceMethod()) + { + await contents.ReadAllAsync(callback, GetSchemaAsync, ct); + } + } + + async Task<(ContentState Value, long Version)> ISnapshotStore.ReadAsync(Guid key) + { + using (Profiler.TraceMethod()) + { + return await contents.ReadAsync(key, GetSchemaAsync); + } + } + + async Task ISnapshotStore.WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion) + { + using (Profiler.TraceMethod()) + { + if (value.SchemaId.Id == Guid.Empty) + { + return; + } + + var schema = await GetSchemaAsync(value.AppId.Id, value.SchemaId.Id); + + var idData = value.Data!.ToMongoModel(schema.SchemaDef, serializer); + var idDraftData = idData; + + if (!ReferenceEquals(value.Data, value.DataDraft)) + { + idDraftData = value.DataDraft.ToMongoModel(schema.SchemaDef, serializer); + } + + var content = SimpleMapper.Map(value, new MongoContentEntity + { + DataByIds = idData, + DataDraftByIds = idDraftData, + IsDeleted = value.IsDeleted, + IndexedAppId = value.AppId.Id, + IndexedSchemaId = value.SchemaId.Id, + ReferencedIds = idData.ToReferencedIds(schema.SchemaDef), + ScheduledAt = value.ScheduleJob?.DueTime, + Version = newVersion + }); + + await contents.UpsertAsync(content, oldVersion); + } + } + + private async Task GetSchemaAsync(Guid appId, Guid schemaId) + { + var schema = await appProvider.GetSchemaAsync(appId, schemaId, true); + + if (schema == null) + { + throw new DomainObjectNotFoundException(schemaId.ToString(), typeof(ISchemaEntity)); + } + + return schema; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/Adapt.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/Adapt.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/Adapt.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/Adapt.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs new file mode 100644 index 000000000..95efcb736 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs @@ -0,0 +1,139 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using MongoDB.Driver; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.MongoDb.Queries; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors +{ + public static class FilterFactory + { + private static readonly FilterDefinitionBuilder Filter = Builders.Filter; + + public static ClrQuery AdjustToModel(this ClrQuery query, Schema schema, bool useDraft) + { + var pathConverter = Adapt.Path(schema, useDraft); + + if (query.Filter != null) + { + query.Filter = query.Filter.Accept(new AdaptionVisitor(pathConverter)); + } + + query.Sort = query.Sort.Select(x => new SortNode(pathConverter(x.Path), x.Order)).ToList(); + + return query; + } + + public static FilterNode? AdjustToModel(this FilterNode filterNode, Schema schema, bool useDraft) + { + var pathConverter = Adapt.Path(schema, useDraft); + + return filterNode.Accept(new AdaptionVisitor(pathConverter)); + } + + public static IFindFluent ContentSort(this IFindFluent cursor, ClrQuery query) + { + return cursor.Sort(query.BuildSort()); + } + + public static IFindFluent ContentTake(this IFindFluent cursor, ClrQuery query) + { + return cursor.Take(query); + } + + public static IFindFluent ContentSkip(this IFindFluent cursor, ClrQuery query) + { + return cursor.Skip(query); + } + + public static IFindFluent WithoutDraft(this IFindFluent cursor, bool includeDraft) + { + return !includeDraft ? cursor.Not(x => x.DataDraftByIds, x => x.IsDeleted) : cursor; + } + + public static FilterDefinition Build(Guid schemaId, Guid id, Status[]? status) + { + return CreateFilter(null, schemaId, new List { id }, status, null); + } + + public static FilterDefinition IdsByApp(Guid appId, ICollection ids, Status[]? status) + { + return CreateFilter(appId, null, ids, status, null); + } + + public static FilterDefinition IdsBySchema(Guid schemaId, ICollection ids, Status[]? status) + { + return CreateFilter(null, schemaId, ids, status, null); + } + + public static FilterDefinition ToFilter(this ClrQuery query, Guid schemaId, ICollection? ids, Status[]? status) + { + return CreateFilter(null, schemaId, ids, status, query); + } + + private static FilterDefinition CreateFilter(Guid? appId, Guid? schemaId, ICollection? ids, Status[]? status, + ClrQuery? query) + { + var filters = new List>(); + + if (appId.HasValue) + { + filters.Add(Filter.Eq(x => x.IndexedAppId, appId.Value)); + } + + if (schemaId.HasValue) + { + filters.Add(Filter.Eq(x => x.IndexedSchemaId, schemaId.Value)); + } + + filters.Add(Filter.Ne(x => x.IsDeleted, true)); + + if (status != null) + { + filters.Add(Filter.In(x => x.Status, status)); + } + + if (ids != null && ids.Count > 0) + { + if (ids.Count > 1) + { + filters.Add(Filter.In(x => x.Id, ids)); + } + else + { + filters.Add(Filter.Eq(x => x.Id, ids.First())); + } + } + + if (query?.Filter != null) + { + filters.Add(query.Filter.BuildFilter()); + } + + return Filter.And(filters); + } + + public static FilterDefinition ToFilter(this FilterNode filterNode, Guid schemaId) + { + var filters = new List> + { + Filter.Eq(x => x.IndexedSchemaId, schemaId), + Filter.Ne(x => x.IsDeleted, true), + filterNode.BuildFilter() + }; + + return Filter.And(filters); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs new file mode 100644 index 000000000..c5a417aef --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules +{ + public sealed class MongoRuleEventEntity : MongoEntity, IRuleEventEntity + { + [BsonRequired] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public Guid AppId { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public Guid RuleId { get; set; } + + [BsonRequired] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public RuleResult Result { get; set; } + + [BsonRequired] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public RuleJobResult JobResult { get; set; } + + [BsonRequired] + [BsonElement] + [BsonJson] + public RuleJob Job { get; set; } + + [BsonRequired] + [BsonElement] + public string? LastDump { get; set; } + + [BsonRequired] + [BsonElement] + public int NumCalls { get; set; } + + [BsonRequired] + [BsonElement] + public Instant Expires { get; set; } + + [BsonRequired] + [BsonElement] + public Instant? NextAttempt { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs new file mode 100644 index 000000000..1deb73603 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs @@ -0,0 +1,136 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules +{ + public sealed class MongoRuleEventRepository : MongoRepositoryBase, IRuleEventRepository + { + private readonly MongoRuleStatisticsCollection statisticsCollection; + + public MongoRuleEventRepository(IMongoDatabase database) + : base(database) + { + statisticsCollection = new MongoRuleStatisticsCollection(database); + } + + protected override string CollectionName() + { + return "RuleEvents"; + } + + protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + await statisticsCollection.InitializeAsync(ct); + + await collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel(Index.Ascending(x => x.NextAttempt)), + new CreateIndexModel(Index.Ascending(x => x.AppId).Descending(x => x.Created)), + new CreateIndexModel( + Index + .Ascending(x => x.Expires), + new CreateIndexOptions + { + ExpireAfter = TimeSpan.Zero + }) + }, ct); + } + + public Task QueryPendingAsync(Instant now, Func callback, CancellationToken ct = default) + { + return Collection.Find(x => x.NextAttempt < now).ForEachAsync(callback, ct); + } + + public async Task> QueryByAppAsync(Guid appId, Guid? ruleId = null, int skip = 0, int take = 20) + { + var filter = Filter.Eq(x => x.AppId, appId); + + if (ruleId.HasValue) + { + filter = Filter.And(filter, Filter.Eq(x => x.RuleId, ruleId)); + } + + var ruleEventEntities = + await Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.Created) + .ToListAsync(); + + return ruleEventEntities; + } + + public async Task FindAsync(Guid id) + { + var ruleEvent = + await Collection.Find(x => x.Id == id) + .FirstOrDefaultAsync(); + + return ruleEvent; + } + + public async Task CountByAppAsync(Guid appId) + { + return (int)await Collection.CountDocumentsAsync(x => x.AppId == appId); + } + + public Task EnqueueAsync(Guid id, Instant nextAttempt) + { + return Collection.UpdateOneAsync(x => x.Id == id, Update.Set(x => x.NextAttempt, nextAttempt)); + } + + public Task EnqueueAsync(RuleJob job, Instant nextAttempt) + { + var entity = SimpleMapper.Map(job, new MongoRuleEventEntity { Job = job, Created = nextAttempt, NextAttempt = nextAttempt }); + + return Collection.InsertOneIfNotExistsAsync(entity); + } + + public Task CancelAsync(Guid id) + { + return Collection.UpdateOneAsync(x => x.Id == id, + Update + .Set(x => x.NextAttempt, null) + .Set(x => x.JobResult, RuleJobResult.Cancelled)); + } + + public async Task MarkSentAsync(RuleJob job, string? dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant finished, Instant? nextCall) + { + if (result == RuleResult.Success) + { + await statisticsCollection.IncrementSuccess(job.AppId, job.RuleId, finished); + } + else + { + await statisticsCollection.IncrementFailed(job.AppId, job.RuleId, finished); + } + + await Collection.UpdateOneAsync(x => x.Id == job.Id, + Update + .Set(x => x.Result, result) + .Set(x => x.LastDump, dump) + .Set(x => x.JobResult, jobResult) + .Set(x => x.NextAttempt, nextCall) + .Inc(x => x.NumCalls, 1)); + } + + public Task> QueryStatisticsByAppAsync(Guid appId) + { + return statisticsCollection.QueryByAppAsync(appId); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj new file mode 100644 index 000000000..35af53605 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj @@ -0,0 +1,32 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs new file mode 100644 index 000000000..239bfcaad --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -0,0 +1,126 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Indexes; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Indexes; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Indexes; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; +using Squidex.Infrastructure.Security; + +namespace Squidex.Domain.Apps.Entities +{ + public sealed class AppProvider : IAppProvider + { + private readonly ILocalCache localCache; + private readonly IAppsIndex indexForApps; + private readonly IRulesIndex indexRules; + private readonly ISchemasIndex indexSchemas; + + public AppProvider(ILocalCache localCache, IAppsIndex indexForApps, IRulesIndex indexRules, ISchemasIndex indexSchemas) + { + Guard.NotNull(indexForApps); + Guard.NotNull(indexRules); + Guard.NotNull(indexSchemas); + Guard.NotNull(localCache); + + this.localCache = localCache; + this.indexForApps = indexForApps; + this.indexRules = indexRules; + this.indexSchemas = indexSchemas; + } + + public Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaAsync(Guid appId, Guid id) + { + return localCache.GetOrCreateAsync($"GetAppWithSchemaAsync({appId}, {id})", async () => + { + return await GetAppWithSchemaUncachedAsync(appId, id); + }); + } + + private async Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaUncachedAsync(Guid appId, Guid id) + { + var app = await GetAppAsync(appId); + + if (app == null) + { + return (null, null); + } + + var schema = await GetSchemaAsync(appId, id, false); + + if (schema == null) + { + return (null, null); + } + + return (app, schema); + } + + public Task GetAppAsync(Guid appId) + { + return localCache.GetOrCreateAsync($"GetAppAsync({appId})", async () => + { + return await indexForApps.GetAppAsync(appId); + }); + } + + public Task GetAppAsync(string appName) + { + return localCache.GetOrCreateAsync($"GetAppAsync({appName})", async () => + { + return await indexForApps.GetAppByNameAsync(appName); + }); + } + + public Task> GetUserAppsAsync(string userId, PermissionSet permissions) + { + return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () => + { + return await indexForApps.GetAppsForUserAsync(userId, permissions); + }); + } + + public Task GetSchemaAsync(Guid appId, string name) + { + return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {name})", async () => + { + return await indexSchemas.GetSchemaByNameAsync(appId, name); + }); + } + + public Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) + { + return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {id}, {allowDeleted})", async () => + { + return await indexSchemas.GetSchemaAsync(appId, id, allowDeleted); + }); + } + + public Task> GetSchemasAsync(Guid appId) + { + return localCache.GetOrCreateAsync($"GetSchemasAsync({appId})", async () => + { + return await indexSchemas.GetSchemasAsync(appId); + }); + } + + public Task> GetRulesAsync(Guid appId) + { + return localCache.GetOrCreateAsync($"GetRulesAsync({appId})", async () => + { + return await indexRules.GetRulesAsync(appId); + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs new file mode 100644 index 000000000..0359af415 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class AppCommandMiddleware : GrainCommandMiddleware + { + private readonly IAssetStore assetStore; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly IContextProvider contextProvider; + + public AppCommandMiddleware( + IGrainFactory grainFactory, + IAssetStore assetStore, + IAssetThumbnailGenerator assetThumbnailGenerator, + IContextProvider contextProvider) + : base(grainFactory) + { + Guard.NotNull(contextProvider); + Guard.NotNull(assetStore); + Guard.NotNull(assetThumbnailGenerator); + + this.assetStore = assetStore; + this.assetThumbnailGenerator = assetThumbnailGenerator; + this.contextProvider = contextProvider; + } + + public override async Task HandleAsync(CommandContext context, Func next) + { + if (context.Command is UploadAppImage uploadImage) + { + await UploadAsync(uploadImage); + } + + await ExecuteCommandAsync(context); + + if (context.PlainResult is IAppEntity app) + { + contextProvider.Context.App = app; + } + + await next(); + } + + private async Task UploadAsync(UploadAppImage uploadImage) + { + var file = uploadImage.File; + + var image = await assetThumbnailGenerator.GetImageInfoAsync(file.OpenRead()); + + if (image == null) + { + throw new ValidationException("File is not an image."); + } + + await assetStore.UploadAsync(uploadImage.AppId.ToString(), file.OpenRead(), true); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs new file mode 100644 index 000000000..5e842433d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs @@ -0,0 +1,510 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Guards; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class AppGrain : DomainObjectGrain, IAppGrain + { + private readonly InitialPatterns initialPatterns; + private readonly IAppPlansProvider appPlansProvider; + private readonly IAppPlanBillingManager appPlansBillingManager; + private readonly IUserResolver userResolver; + + public AppGrain( + InitialPatterns initialPatterns, + IStore store, + ISemanticLog log, + IAppPlansProvider appPlansProvider, + IAppPlanBillingManager appPlansBillingManager, + IUserResolver userResolver) + : base(store, log) + { + Guard.NotNull(initialPatterns); + Guard.NotNull(userResolver); + Guard.NotNull(appPlansProvider); + Guard.NotNull(appPlansBillingManager); + + this.userResolver = userResolver; + this.appPlansProvider = appPlansProvider; + this.appPlansBillingManager = appPlansBillingManager; + this.initialPatterns = initialPatterns; + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + VerifyNotArchived(); + + switch (command) + { + case CreateApp createApp: + return CreateReturn(createApp, c => + { + GuardApp.CanCreate(c); + + Create(c); + + return Snapshot; + }); + + case UpdateApp updateApp: + return UpdateReturn(updateApp, c => + { + GuardApp.CanUpdate(c); + + Update(c); + + return Snapshot; + }); + + case UploadAppImage uploadImage: + return UpdateReturn(uploadImage, c => + { + GuardApp.CanUploadImage(c); + + UploadImage(c); + + return Snapshot; + }); + + case RemoveAppImage removeImage: + return UpdateReturn(removeImage, c => + { + GuardApp.CanRemoveImage(c); + + RemoveImage(c); + + return Snapshot; + }); + + case AssignContributor assignContributor: + return UpdateReturnAsync(assignContributor, async c => + { + await GuardAppContributors.CanAssign(Snapshot.Contributors, Snapshot.Roles, c, userResolver, GetPlan()); + + AssignContributor(c, !Snapshot.Contributors.ContainsKey(assignContributor.ContributorId)); + + return Snapshot; + }); + + case RemoveContributor removeContributor: + return UpdateReturn(removeContributor, c => + { + GuardAppContributors.CanRemove(Snapshot.Contributors, c); + + RemoveContributor(c); + + return Snapshot; + }); + + case AttachClient attachClient: + return UpdateReturn(attachClient, c => + { + GuardAppClients.CanAttach(Snapshot.Clients, c); + + AttachClient(c); + + return Snapshot; + }); + + case UpdateClient updateClient: + return UpdateReturn(updateClient, c => + { + GuardAppClients.CanUpdate(Snapshot.Clients, c, Snapshot.Roles); + + UpdateClient(c); + + return Snapshot; + }); + + case RevokeClient revokeClient: + return UpdateReturn(revokeClient, c => + { + GuardAppClients.CanRevoke(Snapshot.Clients, c); + + RevokeClient(c); + + return Snapshot; + }); + + case AddWorkflow addWorkflow: + return UpdateReturn(addWorkflow, c => + { + GuardAppWorkflows.CanAdd(c); + + AddWorkflow(c); + + return Snapshot; + }); + + case UpdateWorkflow updateWorkflow: + return UpdateReturn(updateWorkflow, c => + { + GuardAppWorkflows.CanUpdate(Snapshot.Workflows, c); + + UpdateWorkflow(c); + + return Snapshot; + }); + + case DeleteWorkflow deleteWorkflow: + return UpdateReturn(deleteWorkflow, c => + { + GuardAppWorkflows.CanDelete(Snapshot.Workflows, c); + + DeleteWorkflow(c); + + return Snapshot; + }); + + case AddLanguage addLanguage: + return UpdateReturn(addLanguage, c => + { + GuardAppLanguages.CanAdd(Snapshot.LanguagesConfig, c); + + AddLanguage(c); + + return Snapshot; + }); + + case RemoveLanguage removeLanguage: + return UpdateReturn(removeLanguage, c => + { + GuardAppLanguages.CanRemove(Snapshot.LanguagesConfig, c); + + RemoveLanguage(c); + + return Snapshot; + }); + + case UpdateLanguage updateLanguage: + return UpdateReturn(updateLanguage, c => + { + GuardAppLanguages.CanUpdate(Snapshot.LanguagesConfig, c); + + UpdateLanguage(c); + + return Snapshot; + }); + + case AddRole addRole: + return UpdateReturn(addRole, c => + { + GuardAppRoles.CanAdd(Snapshot.Roles, c); + + AddRole(c); + + return Snapshot; + }); + + case DeleteRole deleteRole: + return UpdateReturn(deleteRole, c => + { + GuardAppRoles.CanDelete(Snapshot.Roles, c, Snapshot.Contributors, Snapshot.Clients); + + DeleteRole(c); + + return Snapshot; + }); + + case UpdateRole updateRole: + return UpdateReturn(updateRole, c => + { + GuardAppRoles.CanUpdate(Snapshot.Roles, c); + + UpdateRole(c); + + return Snapshot; + }); + + case AddPattern addPattern: + return UpdateReturn(addPattern, c => + { + GuardAppPatterns.CanAdd(Snapshot.Patterns, c); + + AddPattern(c); + + return Snapshot; + }); + + case DeletePattern deletePattern: + return UpdateReturn(deletePattern, c => + { + GuardAppPatterns.CanDelete(Snapshot.Patterns, c); + + DeletePattern(c); + + return Snapshot; + }); + + case UpdatePattern updatePattern: + return UpdateReturn(updatePattern, c => + { + GuardAppPatterns.CanUpdate(Snapshot.Patterns, c); + + UpdatePattern(c); + + return Snapshot; + }); + + case ChangePlan changePlan: + return UpdateReturnAsync(changePlan, async c => + { + GuardApp.CanChangePlan(c, Snapshot.Plan, appPlansProvider); + + if (c.FromCallback) + { + ChangePlan(c); + + return null; + } + else + { + var result = await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.NamedId(), c.PlanId); + + switch (result) + { + case PlanChangedResult _: + ChangePlan(c); + break; + case PlanResetResult _: + ResetPlan(c); + break; + } + + return result; + } + }); + + case ArchiveApp archiveApp: + return UpdateAsync(archiveApp, async c => + { + await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.NamedId(), null); + + ArchiveApp(c); + }); + + default: + throw new NotSupportedException(); + } + } + + private IAppLimitsPlan? GetPlan() + { + return appPlansProvider.GetPlan(Snapshot.Plan?.PlanId); + } + + public void Create(CreateApp command) + { + var appId = NamedId.Of(command.AppId, command.Name); + + var events = new List + { + CreateInitalEvent(command.Name), + CreateInitialOwner(command.Actor), + CreateInitialLanguage() + }; + + foreach (var pattern in initialPatterns) + { + events.Add(CreateInitialPattern(pattern.Key, pattern.Value)); + } + + foreach (var @event in events) + { + @event.Actor = command.Actor; + @event.AppId = appId; + + RaiseEvent(@event); + } + } + + public void UpdateClient(UpdateClient command) + { + if (!string.IsNullOrWhiteSpace(command.Name)) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed())); + } + + if (command.Role != null) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Role = command.Role })); + } + } + + public void Update(UpdateApp command) + { + RaiseEvent(SimpleMapper.Map(command, new AppUpdated())); + } + + public void UploadImage(UploadAppImage command) + { + RaiseEvent(SimpleMapper.Map(command, new AppImageUploaded { Image = new AppImage(command.File.MimeType) })); + } + + public void RemoveImage(RemoveAppImage command) + { + RaiseEvent(SimpleMapper.Map(command, new AppImageRemoved())); + } + + public void UpdateLanguage(UpdateLanguage command) + { + RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated())); + } + + public void AssignContributor(AssignContributor command, bool isAdded) + { + RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned { IsAdded = isAdded })); + } + + public void RemoveContributor(RemoveContributor command) + { + RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved())); + } + + public void AttachClient(AttachClient command) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientAttached())); + } + + public void RevokeClient(RevokeClient command) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked())); + } + + public void AddWorkflow(AddWorkflow command) + { + RaiseEvent(SimpleMapper.Map(command, new AppWorkflowAdded())); + } + + public void UpdateWorkflow(UpdateWorkflow command) + { + RaiseEvent(SimpleMapper.Map(command, new AppWorkflowUpdated())); + } + + public void DeleteWorkflow(DeleteWorkflow command) + { + RaiseEvent(SimpleMapper.Map(command, new AppWorkflowDeleted())); + } + + public void AddLanguage(AddLanguage command) + { + RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded())); + } + + public void RemoveLanguage(RemoveLanguage command) + { + RaiseEvent(SimpleMapper.Map(command, new AppLanguageRemoved())); + } + + public void ChangePlan(ChangePlan command) + { + RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged())); + } + + public void ResetPlan(ChangePlan command) + { + RaiseEvent(SimpleMapper.Map(command, new AppPlanReset())); + } + + public void AddPattern(AddPattern command) + { + RaiseEvent(SimpleMapper.Map(command, new AppPatternAdded())); + } + + public void DeletePattern(DeletePattern command) + { + RaiseEvent(SimpleMapper.Map(command, new AppPatternDeleted())); + } + + public void UpdatePattern(UpdatePattern command) + { + RaiseEvent(SimpleMapper.Map(command, new AppPatternUpdated())); + } + + public void AddRole(AddRole command) + { + RaiseEvent(SimpleMapper.Map(command, new AppRoleAdded())); + } + + public void DeleteRole(DeleteRole command) + { + RaiseEvent(SimpleMapper.Map(command, new AppRoleDeleted())); + } + + public void UpdateRole(UpdateRole command) + { + RaiseEvent(SimpleMapper.Map(command, new AppRoleUpdated())); + } + + public void ArchiveApp(ArchiveApp command) + { + RaiseEvent(SimpleMapper.Map(command, new AppArchived())); + } + + private void VerifyNotArchived() + { + if (Snapshot.IsArchived) + { + throw new DomainException("App has already been archived."); + } + } + + private void RaiseEvent(AppEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = NamedId.Of(Snapshot.Id, Snapshot.Name); + } + + RaiseEvent(Envelope.Create(@event)); + } + + private static AppCreated CreateInitalEvent(string name) + { + return new AppCreated { Name = name }; + } + + private static AppPatternAdded CreateInitialPattern(Guid id, AppPattern pattern) + { + return new AppPatternAdded { PatternId = id, Name = pattern.Name, Pattern = pattern.Pattern, Message = pattern.Message }; + } + + private static AppLanguageAdded CreateInitialLanguage() + { + return new AppLanguageAdded { Language = Language.EN }; + } + + private static AppContributorAssigned CreateInitialOwner(RefToken actor) + { + return new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }; + } + + public Task> GetStateAsync() + { + return J.AsTask(Snapshot); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs new file mode 100644 index 000000000..ed1487a9b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs @@ -0,0 +1,161 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppHistoryEventsCreator : HistoryEventsCreatorBase + { + public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) + { + AddEventMessage( + "assigned {user:[Contributor]} as {[Role]}"); + + AddEventMessage( + "removed {user:[Contributor]} from app"); + + AddEventMessage( + "added client {[Id]} to app"); + + AddEventMessage( + "revoked client {[Id]}"); + + AddEventMessage( + "updated client {[Id]}"); + + AddEventMessage( + "renamed client {[Id]} to {[Name]}"); + + AddEventMessage( + "changed plan to {[Plan]}"); + + AddEventMessage( + "resetted plan"); + + AddEventMessage( + "added language {[Language]}"); + + AddEventMessage( + "removed language {[Language]}"); + + AddEventMessage( + "updated language {[Language]}"); + + AddEventMessage( + "changed master language to {[Language]}"); + + AddEventMessage( + "added pattern {[Name]}"); + + AddEventMessage( + "deleted pattern {[PatternId]}"); + + AddEventMessage( + "updated pattern {[Name]}"); + + AddEventMessage( + "added role {[Name]}"); + + AddEventMessage( + "deleted role {[Name]}"); + + AddEventMessage( + "updated role {[Name]}"); + } + + private HistoryEvent? CreateEvent(IEvent @event) + { + switch (@event) + { + case AppContributorAssigned e: + return CreateContributorsEvent(e, e.ContributorId, e.Role); + case AppContributorRemoved e: + return CreateContributorsEvent(e, e.ContributorId); + case AppClientAttached e: + return CreateClientsEvent(e, e.Id); + case AppClientRenamed e: + return CreateClientsEvent(e, e.Id, ClientName(e)); + case AppClientRevoked e: + return CreateClientsEvent(e, e.Id); + case AppLanguageAdded e: + return CreateLanguagesEvent(e, e.Language); + case AppLanguageUpdated e: + return CreateLanguagesEvent(e, e.Language); + case AppMasterLanguageSet e: + return CreateLanguagesEvent(e, e.Language); + case AppLanguageRemoved e: + return CreateLanguagesEvent(e, e.Language); + case AppPatternAdded e: + return CreatePatternsEvent(e, e.PatternId, e.Name); + case AppPatternUpdated e: + return CreatePatternsEvent(e, e.PatternId, e.Name); + case AppPatternDeleted e: + return CreatePatternsEvent(e, e.PatternId); + case AppRoleAdded e: + return CreateRolesEvent(e, e.Name); + case AppRoleUpdated e: + return CreateRolesEvent(e, e.Name); + case AppRoleDeleted e: + return CreateRolesEvent(e, e.Name); + case AppPlanChanged e: + return CreatePlansEvent(e, e.PlanId); + case AppPlanReset e: + return CreatePlansEvent(e); + } + + return null; + } + + private HistoryEvent CreateContributorsEvent(IEvent e, string contributor, string? role = null) + { + return ForEvent(e, "settings.contributors").Param("Contributor", contributor).Param("Role", role); + } + + private HistoryEvent CreateLanguagesEvent(IEvent e, Language language) + { + return ForEvent(e, "settings.languages").Param("Language", language); + } + + private HistoryEvent CreateRolesEvent(IEvent e, string name) + { + return ForEvent(e, "settings.roles").Param("Name", name); + } + + private HistoryEvent CreatePatternsEvent(IEvent e, Guid id, string? name = null) + { + return ForEvent(e, "settings.patterns").Param("PatternId", id).Param("Name", name); + } + + private HistoryEvent CreateClientsEvent(IEvent e, string id, string? name = null) + { + return ForEvent(e, "settings.clients").Param("Id", id).Param("Name", name); + } + + private HistoryEvent CreatePlansEvent(IEvent e, string? plan = null) + { + return ForEvent(e, "settings.plan").Param("Plan", plan); + } + + protected override Task CreateEventCoreAsync(Envelope @event) + { + return Task.FromResult(CreateEvent(@event.Payload)); + } + + private static string ClientName(AppClientRenamed e) + { + return !string.IsNullOrWhiteSpace(e.Name) ? e.Name : e.Id; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs new file mode 100644 index 000000000..9975c80ad --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class AppUISettings : IAppUISettings + { + private readonly IGrainFactory grainFactory; + + public AppUISettings(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public async Task GetAsync(Guid appId, string? userId) + { + var result = await GetGrain(appId, userId).GetAsync(); + + return result.Value; + } + + public Task RemoveAsync(Guid appId, string? userId, string path) + { + return GetGrain(appId, userId).RemoveAsync(path); + } + + public Task SetAsync(Guid appId, string? userId, string path, IJsonValue value) + { + return GetGrain(appId, userId).SetAsync(path, value.AsJ()); + } + + public Task SetAsync(Guid appId, string? userId, JsonObject settings) + { + return GetGrain(appId, userId).SetAsync(settings.AsJ()); + } + + private IAppUISettingsGrain GetGrain(Guid appId, string? userId) + { + return grainFactory.GetGrain(Key(appId, userId)); + } + + private string Key(Guid appId, string? userId) + { + if (!string.IsNullOrWhiteSpace(userId)) + { + return $"{appId}_{userId}"; + } + else + { + return $"{appId}"; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs new file mode 100644 index 000000000..25952d89e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs @@ -0,0 +1,115 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class AppUISettingsGrain : GrainOfString, IAppUISettingsGrain + { + private readonly IGrainState state; + + [CollectionName("UISettings")] + public sealed class GrainState + { + public JsonObject Settings { get; set; } = JsonValue.Object(); + } + + public AppUISettingsGrain(IGrainState state) + { + Guard.NotNull(state); + + this.state = state; + } + + public Task> GetAsync() + { + return Task.FromResult(state.Value.Settings.AsJ()); + } + + public Task SetAsync(J settings) + { + state.Value.Settings = settings; + + return state.WriteAsync(); + } + + public Task SetAsync(string path, J value) + { + var container = GetContainer(path, true, out var key); + + if (container == null) + { + throw new InvalidOperationException("Path does not lead to an object."); + } + + container[key] = value.Value; + + return state.WriteAsync(); + } + + public async Task RemoveAsync(string path) + { + var container = GetContainer(path, false, out var key); + + if (container?.ContainsKey(key) == true) + { + container.Remove(key); + + await state.WriteAsync(); + } + } + + private JsonObject? GetContainer(string path, bool add, out string key) + { + Guard.NotNullOrEmpty(path); + + var segments = path.Split('.'); + + key = segments[segments.Length - 1]; + + var current = state.Value.Settings; + + if (segments.Length > 1) + { + foreach (var segment in segments.Take(segments.Length - 1)) + { + if (!current.TryGetValue(segment, out var temp)) + { + if (add) + { + temp = JsonValue.Object(); + + current[segment] = temp; + } + else + { + return null; + } + } + + if (temp is JsonObject next) + { + current = next; + } + else + { + return null; + } + } + } + + return current; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs new file mode 100644 index 000000000..4cc3c058b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs @@ -0,0 +1,204 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps.Indexes; +using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class BackupApps : BackupHandler + { + private const string UsersFile = "Users.json"; + private const string SettingsFile = "Settings.json"; + private readonly IAppUISettings appUISettings; + private readonly IAppsIndex appsIndex; + private readonly IUserResolver userResolver; + private readonly HashSet contributors = new HashSet(); + private readonly Dictionary userMapping = new Dictionary(); + private Dictionary usersWithEmail = new Dictionary(); + private string? appReservation; + private string appName; + + public override string Name { get; } = "Apps"; + + public BackupApps(IAppUISettings appUISettings, IAppsIndex appsIndex, IUserResolver userResolver) + { + Guard.NotNull(appsIndex); + Guard.NotNull(appUISettings); + Guard.NotNull(userResolver); + + this.appsIndex = appsIndex; + this.appUISettings = appUISettings; + this.userResolver = userResolver; + } + + public override async Task BackupEventAsync(Envelope @event, Guid appId, BackupWriter writer) + { + if (@event.Payload is AppContributorAssigned appContributorAssigned) + { + var userId = appContributorAssigned.ContributorId; + + if (!usersWithEmail.ContainsKey(userId)) + { + var user = await userResolver.FindByIdOrEmailAsync(userId); + + if (user != null) + { + usersWithEmail.Add(userId, user.Email); + } + } + } + } + + public override async Task BackupAsync(Guid appId, BackupWriter writer) + { + await WriteUsersAsync(writer); + await WriteSettingsAsync(writer, appId); + } + + public override async Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) + { + switch (@event.Payload) + { + case AppCreated appCreated: + { + appName = appCreated.Name; + + await ResolveUsersAsync(reader); + await ReserveAppAsync(appId); + + break; + } + + case AppContributorAssigned contributorAssigned: + { + if (!userMapping.TryGetValue(contributorAssigned.ContributorId, out var user) || user.Equals(actor)) + { + return false; + } + + contributorAssigned.ContributorId = user.Identifier; + contributors.Add(contributorAssigned.ContributorId); + break; + } + + case AppContributorRemoved contributorRemoved: + { + if (!userMapping.TryGetValue(contributorRemoved.ContributorId, out var user) || user.Equals(actor)) + { + return false; + } + + contributorRemoved.ContributorId = user.Identifier; + contributors.Remove(contributorRemoved.ContributorId); + break; + } + } + + if (@event.Payload is SquidexEvent squidexEvent) + { + squidexEvent.Actor = MapUser(squidexEvent.Actor.Identifier, actor); + } + + return true; + } + + public override Task RestoreAsync(Guid appId, BackupReader reader) + { + return ReadSettingsAsync(reader, appId); + } + + private async Task ReserveAppAsync(Guid appId) + { + appReservation = await appsIndex.ReserveAsync(appId, appName); + + if (appReservation == null) + { + throw new BackupRestoreException("The app id or name is not available."); + } + } + + public override async Task CleanupRestoreErrorAsync(Guid appId) + { + if (appReservation != null) + { + await appsIndex.RemoveReservationAsync(appReservation); + } + } + + private RefToken MapUser(string userId, RefToken fallback) + { + return userMapping.GetOrAdd(userId, fallback); + } + + private async Task ResolveUsersAsync(BackupReader reader) + { + await ReadUsersAsync(reader); + + foreach (var kvp in usersWithEmail) + { + var email = kvp.Value; + + var user = await userResolver.FindByIdOrEmailAsync(email); + + if (user == null && await userResolver.CreateUserIfNotExists(kvp.Value)) + { + user = await userResolver.FindByIdOrEmailAsync(email); + } + + if (user != null) + { + userMapping[kvp.Key] = new RefToken(RefTokenType.Subject, user.Id); + } + } + } + + private async Task ReadUsersAsync(BackupReader reader) + { + var json = await reader.ReadJsonAttachmentAsync>(UsersFile); + + usersWithEmail = json; + } + + private async Task WriteUsersAsync(BackupWriter writer) + { + var json = usersWithEmail; + + await writer.WriteJsonAsync(UsersFile, json); + } + + private async Task WriteSettingsAsync(BackupWriter writer, Guid appId) + { + var json = await appUISettings.GetAsync(appId, null); + + await writer.WriteJsonAsync(SettingsFile, json); + } + + private async Task ReadSettingsAsync(BackupReader reader, Guid appId) + { + var json = await reader.ReadJsonAttachmentAsync(SettingsFile); + + await appUISettings.SetAsync(appId, null, json); + } + + public override async Task CompleteRestoreAsync(Guid appId, BackupReader reader) + { + await appsIndex.AddAsync(appReservation); + + await appsIndex.RebuildByContributorsAsync(appId, contributors); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs new file mode 100644 index 000000000..9f74f6291 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class AddPattern : AppCommand + { + public Guid PatternId { get; set; } + + public string Name { get; set; } + + public string Pattern { get; set; } + + public string? Message { get; set; } + + public AddPattern() + { + PatternId = Guid.NewGuid(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/ArchiveApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ArchiveApp.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/ArchiveApp.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ArchiveApp.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs new file mode 100644 index 000000000..f47952812 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class UpdateApp : AppCommand + { + public string? Label { get; set; } + + public string? Description { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs new file mode 100644 index 000000000..0f28418f9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class UpdatePattern : AppCommand + { + public Guid PatternId { get; set; } + + public string Name { get; set; } + + public string Pattern { get; set; } + + public string? Message { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs new file mode 100644 index 000000000..a0f8e9142 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class DefaultAppLogStore : IAppLogStore + { + private readonly ILogStore logStore; + + public DefaultAppLogStore(ILogStore logStore) + { + Guard.NotNull(logStore); + + this.logStore = logStore; + } + + public Task ReadLogAsync(string appId, DateTime from, DateTime to, Stream stream) + { + Guard.NotNull(appId); + + return logStore.ReadLogAsync(appId, from, to, stream); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs new file mode 100644 index 000000000..a70ca3f61 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Indexes; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics +{ + public sealed class OrleansAppsHealthCheck : IHealthCheck + { + private readonly IAppsByNameIndexGrain index; + + public OrleansAppsHealthCheck(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + index = grainFactory.GetGrain(SingleGrain.Id); + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + await index.CountAsync(); + + return HealthCheckResult.Healthy("Orleans must establish communication."); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs new file mode 100644 index 000000000..02239fde6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardApp + { + public static void CanCreate(CreateApp command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot create app.", e => + { + if (!command.Name.IsSlug()) + { + e(Not.ValidSlug("Name"), nameof(command.Name)); + } + }); + } + + public static void CanUploadImage(UploadAppImage command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot upload image.", e => + { + if (command.File == null) + { + e(Not.Defined("File"), nameof(command.File)); + } + }); + } + + public static void CanUpdate(UpdateApp command) + { + Guard.NotNull(command); + } + + public static void CanRemoveImage(RemoveAppImage command) + { + Guard.NotNull(command); + } + + public static void CanChangePlan(ChangePlan command, AppPlan? plan, IAppPlansProvider appPlans) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot change plan.", e => + { + if (string.IsNullOrWhiteSpace(command.PlanId)) + { + e(Not.Defined("Plan id"), nameof(command.PlanId)); + return; + } + + if (appPlans.GetPlan(command.PlanId) == null) + { + e("A plan with this id does not exist.", nameof(command.PlanId)); + } + + if (!string.IsNullOrWhiteSpace(command.PlanId) && plan != null && !plan.Owner.Equals(command.Actor)) + { + e("Plan can only changed from the user who configured the plan initially."); + } + + if (string.Equals(command.PlanId, plan?.PlanId, StringComparison.OrdinalIgnoreCase)) + { + e("App has already this plan."); + } + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs new file mode 100644 index 000000000..a14c9cffe --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppClients + { + public static void CanAttach(AppClients clients, AttachClient command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot attach client.", e => + { + if (string.IsNullOrWhiteSpace(command.Id)) + { + e(Not.Defined("Client id"), nameof(command.Id)); + } + else if (clients.ContainsKey(command.Id)) + { + e("A client with the same id already exists."); + } + }); + } + + public static void CanRevoke(AppClients clients, RevokeClient command) + { + Guard.NotNull(command); + + GetClientOrThrow(clients, command.Id); + + Validate.It(() => "Cannot revoke client.", e => + { + if (string.IsNullOrWhiteSpace(command.Id)) + { + e(Not.Defined("Client id"), nameof(command.Id)); + } + }); + } + + public static void CanUpdate(AppClients clients, UpdateClient command, Roles roles) + { + Guard.NotNull(command); + + var client = GetClientOrThrow(clients, command.Id); + + Validate.It(() => "Cannot update client.", e => + { + if (string.IsNullOrWhiteSpace(command.Id)) + { + e(Not.Defined("Client id"), nameof(command.Id)); + } + + if (string.IsNullOrWhiteSpace(command.Name) && command.Role == null) + { + e(Not.DefinedOr("name", "role"), nameof(command.Name), nameof(command.Role)); + } + + if (command.Role != null && !roles.Contains(command.Role)) + { + e(Not.Valid("role"), nameof(command.Role)); + } + + if (client == null) + { + return; + } + + if (!string.IsNullOrWhiteSpace(command.Name) && string.Equals(client.Name, command.Name)) + { + e(Not.New("Client", "name"), nameof(command.Name)); + } + + if (command.Role == client.Role) + { + e(Not.New("Client", "role"), nameof(command.Role)); + } + }); + } + + private static AppClient? GetClientOrThrow(AppClients clients, string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + return null; + } + + if (!clients.TryGetValue(id, out var client)) + { + throw new DomainObjectNotFoundException(id, "Clients", typeof(IAppEntity)); + } + + return client; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs new file mode 100644 index 000000000..88c5240a0 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs @@ -0,0 +1,99 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppContributors + { + public static Task CanAssign(AppContributors contributors, Roles roles, AssignContributor command, IUserResolver users, IAppLimitsPlan? plan) + { + Guard.NotNull(command); + + return Validate.It(() => "Cannot assign contributor.", async e => + { + if (!roles.Contains(command.Role)) + { + e(Not.Valid("role"), nameof(command.Role)); + } + + if (string.IsNullOrWhiteSpace(command.ContributorId)) + { + e(Not.Defined("Contributor id"), nameof(command.ContributorId)); + } + else + { + var user = await users.FindByIdOrEmailAsync(command.ContributorId); + + if (user == null) + { + throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity)); + } + + command.ContributorId = user.Id; + + if (!command.IsRestore) + { + if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase)) + { + throw new DomainForbiddenException("You cannot change your own role."); + } + + if (contributors.TryGetValue(command.ContributorId, out var role)) + { + if (role == command.Role) + { + e(Not.New("Contributor", "role"), nameof(command.Role)); + } + } + else + { + if (plan != null && plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors) + { + e("You have reached the maximum number of contributors for your plan."); + } + } + } + } + }); + } + + public static void CanRemove(AppContributors contributors, RemoveContributor command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot remove contributor.", e => + { + if (string.IsNullOrWhiteSpace(command.ContributorId)) + { + e(Not.Defined("Contributor id"), nameof(command.ContributorId)); + } + + var ownerIds = contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToList(); + + if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId)) + { + e("Cannot remove the only owner."); + } + }); + + if (!contributors.ContainsKey(command.ContributorId)) + { + throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity)); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs new file mode 100644 index 000000000..f924941dd --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppLanguages + { + public static void CanAdd(LanguagesConfig languages, AddLanguage command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot add language.", e => + { + if (command.Language == null) + { + e(Not.Defined("Language code"), nameof(command.Language)); + } + else if (languages.Contains(command.Language)) + { + e("Language has already been added."); + } + }); + } + + public static void CanRemove(LanguagesConfig languages, RemoveLanguage command) + { + Guard.NotNull(command); + + var config = GetConfigOrThrow(languages, command.Language); + + Validate.It(() => "Cannot remove language.", e => + { + if (command.Language == null) + { + e(Not.Defined("Language code"), nameof(command.Language)); + } + + if (languages.Master == config) + { + e("Master language cannot be removed."); + } + }); + } + + public static void CanUpdate(LanguagesConfig languages, UpdateLanguage command) + { + Guard.NotNull(command); + + var config = GetConfigOrThrow(languages, command.Language); + + Validate.It(() => "Cannot update language.", e => + { + if (command.Language == null) + { + e(Not.Defined("Language code"), nameof(command.Language)); + } + + if ((languages.Master == config || command.IsMaster) && command.IsOptional) + { + e("Master language cannot be made optional.", nameof(command.IsMaster)); + } + + if (command.Fallback == null) + { + return; + } + + foreach (var fallback in command.Fallback) + { + if (!languages.Contains(fallback)) + { + e($"App does not have fallback language '{fallback}'.", nameof(command.Fallback)); + } + } + }); + } + + private static LanguageConfig? GetConfigOrThrow(LanguagesConfig languages, Language language) + { + if (language == null) + { + return null; + } + + if (!languages.TryGetConfig(language, out var languageConfig)) + { + throw new DomainObjectNotFoundException(language, "Languages", typeof(IAppEntity)); + } + + return languageConfig; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs new file mode 100644 index 000000000..0cdbe18f0 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== +using System; +using System.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppPatterns + { + public static void CanAdd(AppPatterns patterns, AddPattern command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot add pattern.", e => + { + if (command.PatternId == Guid.Empty) + { + e(Not.Defined("Id"), nameof(command.PatternId)); + } + + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined("Name"), nameof(command.Name)); + } + + if (patterns.Values.Any(x => x.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase))) + { + e("A pattern with the same name already exists."); + } + + if (string.IsNullOrWhiteSpace(command.Pattern)) + { + e(Not.Defined("Pattern"), nameof(command.Pattern)); + } + else if (!command.Pattern.IsValidRegex()) + { + e(Not.Valid("Pattern"), nameof(command.Pattern)); + } + + if (patterns.Values.Any(x => x.Pattern == command.Pattern)) + { + e("This pattern already exists but with another name."); + } + }); + } + + public static void CanDelete(AppPatterns patterns, DeletePattern command) + { + Guard.NotNull(command); + + if (!patterns.ContainsKey(command.PatternId)) + { + throw new DomainObjectNotFoundException(command.PatternId.ToString(), typeof(AppPattern)); + } + } + + public static void CanUpdate(AppPatterns patterns, UpdatePattern command) + { + Guard.NotNull(command); + + if (!patterns.ContainsKey(command.PatternId)) + { + throw new DomainObjectNotFoundException(command.PatternId.ToString(), typeof(AppPattern)); + } + + Validate.It(() => "Cannot update pattern.", e => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined("Name"), nameof(command.Name)); + } + + if (patterns.Any(x => x.Key != command.PatternId && x.Value.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase))) + { + e("A pattern with the same name already exists."); + } + + if (string.IsNullOrWhiteSpace(command.Pattern)) + { + e(Not.Defined("Pattern"), nameof(command.Pattern)); + } + else if (!command.Pattern.IsValidRegex()) + { + e(Not.Valid("Pattern"), nameof(command.Pattern)); + } + + if (patterns.Any(x => x.Key != command.PatternId && x.Value.Pattern == command.Pattern)) + { + e("This pattern already exists but with another name."); + } + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs new file mode 100644 index 000000000..929bd0692 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppRoles + { + public static void CanAdd(Roles roles, AddRole command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot add role.", e => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined("Name"), nameof(command.Name)); + } + else if (roles.Contains(command.Name)) + { + e("A role with the same name already exists."); + } + }); + } + + public static void CanDelete(Roles roles, DeleteRole command, AppContributors contributors, AppClients clients) + { + Guard.NotNull(command); + + CheckRoleExists(roles, command.Name); + + Validate.It(() => "Cannot delete role.", e => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined("Name"), nameof(command.Name)); + } + else if (Roles.IsDefault(command.Name)) + { + e("Cannot delete a default role."); + } + + if (clients.Values.Any(x => string.Equals(x.Role, command.Name, StringComparison.OrdinalIgnoreCase))) + { + e("Cannot remove a role when a client is assigned."); + } + + if (contributors.Values.Any(x => string.Equals(x, command.Name, StringComparison.OrdinalIgnoreCase))) + { + e("Cannot remove a role when a contributor is assigned."); + } + }); + } + + public static void CanUpdate(Roles roles, UpdateRole command) + { + Guard.NotNull(command); + + CheckRoleExists(roles, command.Name); + + Validate.It(() => "Cannot delete role.", e => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined("Name"), nameof(command.Name)); + } + else if (Roles.IsDefault(command.Name)) + { + e("Cannot update a default role."); + } + + if (command.Permissions == null) + { + e(Not.Defined("Permissions"), nameof(command.Permissions)); + } + }); + } + + private static void CheckRoleExists(Roles roles, string name) + { + if (string.IsNullOrWhiteSpace(name) || Roles.IsDefault(name)) + { + return; + } + + if (!roles.ContainsCustom(name)) + { + throw new DomainObjectNotFoundException(name, "Roles", typeof(IAppEntity)); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs new file mode 100644 index 000000000..c0a6c90ee --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppWorkflows + { + public static void CanAdd(AddWorkflow command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot add workflow.", e => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined("Name"), nameof(command.Name)); + } + }); + } + + public static void CanUpdate(Workflows workflows, UpdateWorkflow command) + { + Guard.NotNull(command); + + CheckWorkflowExists(workflows, command.WorkflowId); + + Validate.It(() => "Cannot update workflow.", e => + { + if (command.Workflow == null) + { + e(Not.Defined("Workflow"), nameof(command.Workflow)); + return; + } + + var workflow = command.Workflow; + + if (!workflow.Steps.ContainsKey(workflow.Initial)) + { + e(Not.Defined("Initial step"), $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}"); + } + + if (workflow.Initial == Status.Published) + { + e("Initial step cannot be published step.", $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}"); + } + + var stepsPrefix = $"{nameof(command.Workflow)}.{nameof(workflow.Steps)}"; + + if (!workflow.Steps.ContainsKey(Status.Published)) + { + e("Workflow must have a published step.", stepsPrefix); + } + + foreach (var step in workflow.Steps) + { + var stepPrefix = $"{stepsPrefix}.{step.Key}"; + + if (step.Value == null) + { + e(Not.Defined("Step"), stepPrefix); + } + else + { + foreach (var transition in step.Value.Transitions) + { + var transitionPrefix = $"{stepPrefix}.{nameof(step.Value.Transitions)}.{transition.Key}"; + + if (!workflow.Steps.ContainsKey(transition.Key)) + { + e("Transition has an invalid target.", transitionPrefix); + } + + if (transition.Value == null) + { + e(Not.Defined("Transition"), transitionPrefix); + } + } + } + } + }); + } + + public static void CanDelete(Workflows workflows, DeleteWorkflow command) + { + Guard.NotNull(command); + + CheckWorkflowExists(workflows, command.WorkflowId); + } + + private static void CheckWorkflowExists(Workflows workflows, Guid id) + { + if (!workflows.ContainsKey(id)) + { + throw new DomainObjectNotFoundException(id.ToString(), "Workflows", typeof(IAppEntity)); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs new file mode 100644 index 000000000..3e288efba --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public interface IAppEntity : + IEntity, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion + { + string Name { get; } + + string? Label { get; } + + string? Description { get; } + + Roles Roles { get; } + + AppPlan? Plan { get; } + + AppImage? Image { get; } + + AppClients Clients { get; } + + AppPatterns Patterns { get; } + + AppContributors Contributors { get; } + + LanguagesConfig LanguagesConfig { get; } + + Workflows Workflows { get; } + + bool IsArchived { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/IAppGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/IAppGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs new file mode 100644 index 000000000..b6a46d78f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public interface IAppUISettings + { + Task GetAsync(Guid appId, string? userId); + + Task SetAsync(Guid appId, string? userId, string path, IJsonValue value); + + Task SetAsync(Guid appId, string? userId, JsonObject settings); + + Task RemoveAsync(Guid appId, string? userId, string path); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs new file mode 100644 index 000000000..ce416a3c1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs @@ -0,0 +1,286 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.Validation; +using Squidex.Shared; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsIndex : IAppsIndex, ICommandMiddleware + { + private readonly IGrainFactory grainFactory; + + public AppsIndex(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public async Task RebuildByContributorsAsync(Guid appId, HashSet contributors) + { + foreach (var contributorId in contributors) + { + await Index(contributorId).AddAsync(appId); + } + } + + public Task RebuildByContributorsAsync(string contributorId, HashSet apps) + { + return Index(contributorId).RebuildAsync(apps); + } + + public Task RebuildAsync(Dictionary appsByName) + { + return Index().RebuildAsync(appsByName); + } + + public Task RemoveReservationAsync(string? token) + { + return Index().RemoveReservationAsync(token); + } + + public Task> GetIdsAsync() + { + return Index().GetIdsAsync(); + } + + public Task AddAsync(string? token) + { + return Index().AddAsync(token); + } + + public Task ReserveAsync(Guid id, string name) + { + return Index().ReserveAsync(id, name); + } + + public async Task> GetAppsAsync() + { + using (Profiler.TraceMethod()) + { + var ids = await GetAppIdsAsync(); + + var apps = + await Task.WhenAll(ids + .Select(id => GetAppAsync(id))); + + return apps.Where(x => x != null).ToList(); + } + } + + public async Task> GetAppsForUserAsync(string userId, PermissionSet permissions) + { + using (Profiler.TraceMethod()) + { + var ids = + await Task.WhenAll( + GetAppIdsByUserAsync(userId), + GetAppIdsAsync(permissions.ToAppNames())); + + var apps = + await Task.WhenAll(ids + .SelectMany(x => x) + .Select(id => GetAppAsync(id))); + + return apps.Where(x => x != null).ToList(); + } + } + + public async Task GetAppByNameAsync(string name) + { + using (Profiler.TraceMethod()) + { + var appId = await GetAppIdAsync(name); + + if (appId == default) + { + return null; + } + + return await GetAppAsync(appId); + } + } + + public async Task GetAppAsync(Guid appId) + { + using (Profiler.TraceMethod()) + { + var app = await grainFactory.GetGrain(appId).GetStateAsync(); + + if (IsFound(app.Value)) + { + return app.Value; + } + + return null; + } + } + + private async Task> GetAppIdsByUserAsync(string userId) + { + using (Profiler.TraceMethod()) + { + return await grainFactory.GetGrain(userId).GetIdsAsync(); + } + } + + private async Task> GetAppIdsAsync() + { + using (Profiler.TraceMethod()) + { + return await grainFactory.GetGrain(SingleGrain.Id).GetIdsAsync(); + } + } + + private async Task> GetAppIdsAsync(string[] names) + { + using (Profiler.TraceMethod()) + { + return await grainFactory.GetGrain(SingleGrain.Id).GetIdsAsync(names); + } + } + + private async Task GetAppIdAsync(string name) + { + using (Profiler.TraceMethod()) + { + return await grainFactory.GetGrain(SingleGrain.Id).GetIdAsync(name); + } + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.Command is CreateApp createApp) + { + var index = Index(); + + var token = await CheckAppAsync(index, createApp); + + try + { + await next(); + } + finally + { + if (token != null) + { + if (context.IsCompleted) + { + await index.AddAsync(token); + + if (createApp.Actor.IsSubject) + { + await Index(createApp.Actor.Identifier).AddAsync(createApp.AppId); + } + } + else + { + await index.RemoveReservationAsync(token); + } + } + } + } + else + { + await next(); + + if (context.IsCompleted) + { + switch (context.Command) + { + case AssignContributor assignContributor: + await AssignContributorAsync(assignContributor); + break; + + case RemoveContributor removeContributor: + await RemoveContributorAsync(removeContributor); + break; + + case ArchiveApp archiveApp: + await ArchiveAppAsync(archiveApp); + break; + } + } + } + } + + private async Task CheckAppAsync(IAppsByNameIndexGrain index, CreateApp command) + { + var name = command.Name; + + if (name.IsSlug()) + { + var token = await index.ReserveAsync(command.AppId, name); + + if (token == null) + { + var error = new ValidationError("An app with this already exists."); + + throw new ValidationException("Cannot create app.", error); + } + + return token; + } + + return null; + } + + private Task AssignContributorAsync(AssignContributor command) + { + return Index(command.ContributorId).AddAsync(command.AppId); + } + + private Task RemoveContributorAsync(RemoveContributor command) + { + return Index(command.ContributorId).RemoveAsync(command.AppId); + } + + private async Task ArchiveAppAsync(ArchiveApp command) + { + var appId = command.AppId; + + var app = await grainFactory.GetGrain(appId).GetStateAsync(); + + if (IsFound(app.Value)) + { + await Index().RemoveAsync(appId); + } + + foreach (var contributorId in app.Value.Contributors.Keys) + { + await Index(contributorId).RemoveAsync(appId); + } + } + + private static bool IsFound(IAppEntity app) + { + return app.Version > EtagVersion.Empty && !app.IsArchived; + } + + private IAppsByNameIndexGrain Index() + { + return grainFactory.GetGrain(SingleGrain.Id); + } + + private IAppsByUserIndexGrain Index(string id) + { + return grainFactory.GetGrain(id); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs new file mode 100644 index 000000000..383c349d3 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure.Security; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public interface IAppsIndex + { + Task> GetIdsAsync(); + + Task> GetAppsAsync(); + + Task> GetAppsForUserAsync(string userId, PermissionSet permissions); + + Task GetAppByNameAsync(string name); + + Task GetAppAsync(Guid appId); + + Task ReserveAsync(Guid id, string name); + + Task AddAsync(string? token); + + Task RemoveReservationAsync(string? token); + + Task RebuildByContributorsAsync(string contributorId, HashSet apps); + + Task RebuildAsync(Dictionary apps); + + Task RebuildByContributorsAsync(Guid appId, HashSet contributors); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs new file mode 100644 index 000000000..a0362cb85 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs @@ -0,0 +1,52 @@ +// ========================================================================== +// 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.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Apps.Invitation +{ + public sealed class InviteUserCommandMiddleware : ICommandMiddleware + { + private readonly IUserResolver userResolver; + + public InviteUserCommandMiddleware(IUserResolver userResolver) + { + Guard.NotNull(userResolver); + + this.userResolver = userResolver; + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.Command is AssignContributor assignContributor && ShouldInvite(assignContributor)) + { + var created = await userResolver.CreateUserIfNotExists(assignContributor.ContributorId, true); + + await next(); + + if (created && context.PlainResult is IAppEntity app) + { + context.Complete(new InvitedResult { App = app }); + } + } + else + { + await next(); + } + } + + private static bool ShouldInvite(AssignContributor assignContributor) + { + return assignContributor.Invite && assignContributor.ContributorId.IsEmail(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs new file mode 100644 index 000000000..c2c2e100f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs @@ -0,0 +1,76 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Squidex.Shared; + +#pragma warning disable IDE0028 // Simplify collection initialization + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class RolePermissionsProvider + { + private readonly IAppProvider appProvider; + + public RolePermissionsProvider(IAppProvider appProvider) + { + Guard.NotNull(appProvider); + + this.appProvider = appProvider; + } + + public async Task> GetPermissionsAsync(IAppEntity app) + { + var schemaNames = await GetSchemaNamesAsync(app); + + var result = new List { Permission.Any }; + + foreach (var permission in Permissions.ForAppsNonSchema) + { + if (permission.Length > Permissions.App.Length + 1) + { + var trimmed = permission.Substring(Permissions.App.Length + 1); + + if (trimmed.Length > 0) + { + result.Add(trimmed); + } + } + } + + foreach (var permission in Permissions.ForAppsSchema) + { + var trimmed = permission.Substring(Permissions.App.Length + 1); + + foreach (var schema in schemaNames) + { + var replaced = trimmed.Replace("{name}", schema); + + result.Add(replaced); + } + } + + return result; + } + + private async Task> GetSchemaNamesAsync(IAppEntity app) + { + var schemas = await appProvider.GetSchemasAsync(app.Id); + + var schemaNames = new List(); + + schemaNames.Add(Permission.Any); + schemaNames.AddRange(schemas.Select(x => x.SchemaDef.Name)); + + return schemaNames; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs new file mode 100644 index 000000000..764150bbd --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Services +{ + public interface IAppLimitsPlan + { + string Id { get; } + + string Name { get; } + + string Costs { get; } + + string? YearlyCosts { get; } + + string? YearlyId { get; } + + long MaxApiCalls { get; } + + long MaxAssetSize { get; } + + int MaxContributors { get; } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs new file mode 100644 index 000000000..3a36705ba --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Services +{ + public interface IAppPlanBillingManager + { + bool HasPortal { get; } + + Task ChangePlanAsync(string userId, NamedId appId, string? planId); + + Task GetPortalLinkAsync(string userId); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs new file mode 100644 index 000000000..c73aa5dc1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Entities.Apps.Services +{ + public interface IAppPlansProvider + { + IEnumerable GetAvailablePlans(); + + bool IsConfiguredPlan(string? planId); + + IAppLimitsPlan? GetPlanUpgradeForApp(IAppEntity app); + + IAppLimitsPlan? GetPlanUpgrade(string? planId); + + IAppLimitsPlan? GetPlan(string? planId); + + IAppLimitsPlan GetPlanForApp(IAppEntity app); + + IAppLimitsPlan GetFreePlan(); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/IChangePlanResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IChangePlanResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Services/IChangePlanResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IChangePlanResult.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs new file mode 100644 index 000000000..1548f3ea6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations +{ + public sealed class ConfigAppLimitsPlan : IAppLimitsPlan + { + public string Id { get; set; } + + public string Name { get; set; } + + public string Costs { get; set; } + + public string? YearlyCosts { get; set; } + + public string? YearlyId { get; set; } + + public long MaxApiCalls { get; set; } + + public long MaxAssetSize { get; set; } + + public int MaxContributors { get; set; } + + public ConfigAppLimitsPlan Clone() + { + return (ConfigAppLimitsPlan)MemberwiseClone(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs new file mode 100644 index 000000000..3be58073c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs @@ -0,0 +1,98 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations +{ + public sealed class ConfigAppPlansProvider : IAppPlansProvider + { + private static readonly ConfigAppLimitsPlan Infinite = new ConfigAppLimitsPlan + { + Id = "infinite", + Name = "Infinite", + MaxApiCalls = -1, + MaxAssetSize = -1, + MaxContributors = -1 + }; + + private readonly Dictionary plansById = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly List plansList = new List(); + + public ConfigAppPlansProvider(IEnumerable config) + { + Guard.NotNull(config); + + foreach (var plan in config.OrderBy(x => x.MaxApiCalls).Select(x => x.Clone())) + { + plansList.Add(plan); + plansById[plan.Id] = plan; + + if (!string.IsNullOrWhiteSpace(plan.YearlyId) && !string.IsNullOrWhiteSpace(plan.YearlyCosts)) + { + plansById[plan.YearlyId] = plan; + } + } + } + + public IEnumerable GetAvailablePlans() + { + return plansList; + } + + public bool IsConfiguredPlan(string? planId) + { + return planId != null && plansById.ContainsKey(planId); + } + + public IAppLimitsPlan? GetPlan(string? planId) + { + return plansById.GetOrDefault(planId ?? string.Empty); + } + + public IAppLimitsPlan GetPlanForApp(IAppEntity app) + { + Guard.NotNull(app); + + return GetPlanCore(app.Plan?.PlanId); + } + + public IAppLimitsPlan GetFreePlan() + { + return GetPlanCore(plansList.FirstOrDefault(x => string.IsNullOrWhiteSpace(x.Costs))?.Id); + } + + public IAppLimitsPlan? GetPlanUpgradeForApp(IAppEntity app) + { + Guard.NotNull(app); + + return GetPlanUpgrade(app.Plan?.PlanId); + } + + public IAppLimitsPlan? GetPlanUpgrade(string? planId) + { + var plan = GetPlanCore(planId); + + var nextPlanIndex = plansList.IndexOf(plan); + + if (nextPlanIndex >= 0 && nextPlanIndex < plansList.Count - 1) + { + return plansList[nextPlanIndex + 1]; + } + + return null; + } + + private ConfigAppLimitsPlan GetPlanCore(string? planId) + { + return plansById.GetOrDefault(planId ?? string.Empty) ?? plansById.Values.FirstOrDefault() ?? Infinite; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs new file mode 100644 index 000000000..418aa4814 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations +{ + public sealed class NoopAppPlanBillingManager : IAppPlanBillingManager + { + public bool HasPortal + { + get { return false; } + } + + public Task ChangePlanAsync(string userId, NamedId appId, string? planId) + { + return Task.FromResult(new PlanResetResult()); + } + + public Task GetPortalLinkAsync(string userId) + { + return Task.FromResult(string.Empty); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanResetResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanResetResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Services/PlanResetResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanResetResult.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs new file mode 100644 index 000000000..956ffb1a9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Services +{ + public sealed class RedirectToCheckoutResult : IChangePlanResult + { + public Uri Url { get; } + + public RedirectToCheckoutResult(Uri url) + { + Guard.NotNull(url); + + Url = url; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs new file mode 100644 index 000000000..fb600d5b7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -0,0 +1,252 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Runtime.Serialization; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +#pragma warning disable IDE0060 // Remove unused parameter + +namespace Squidex.Domain.Apps.Entities.Apps.State +{ + [CollectionName("Apps")] + public class AppState : DomainObjectState, IAppEntity + { + [DataMember] + public string Name { get; set; } + + [DataMember] + public string Label { get; set; } + + [DataMember] + public string Description { get; set; } + + [DataMember] + public Roles Roles { get; set; } = Roles.Empty; + + [DataMember] + public AppPlan? Plan { get; set; } + + [DataMember] + public AppImage? Image { get; set; } + + [DataMember] + public AppClients Clients { get; set; } = AppClients.Empty; + + [DataMember] + public AppPatterns Patterns { get; set; } = AppPatterns.Empty; + + [DataMember] + public AppContributors Contributors { get; set; } = AppContributors.Empty; + + [DataMember] + public LanguagesConfig LanguagesConfig { get; set; } = LanguagesConfig.English; + + [DataMember] + public Workflows Workflows { get; set; } = Workflows.Empty; + + [DataMember] + public bool IsArchived { get; set; } + + public void ApplyEvent(IEvent @event) + { + switch (@event) + { + case AppCreated e: + { + SimpleMapper.Map(e, this); + + break; + } + + case AppUpdated e: + { + SimpleMapper.Map(e, this); + + break; + } + + case AppImageUploaded e: + { + Image = e.Image; + + break; + } + + case AppImageRemoved _: + { + Image = null; + + break; + } + + case AppPlanChanged e: + { + Plan = AppPlan.Build(e.Actor, e.PlanId); + + break; + } + + case AppPlanReset _: + { + Plan = null; + + break; + } + + case AppContributorAssigned e: + { + Contributors = Contributors.Assign(e.ContributorId, e.Role); + + break; + } + + case AppContributorRemoved e: + { + Contributors = Contributors.Remove(e.ContributorId); + + break; + } + + case AppClientAttached e: + { + Clients = Clients.Add(e.Id, e.Secret); + + break; + } + + case AppClientUpdated e: + { + Clients = Clients.Update(e.Id, e.Role); + + break; + } + + case AppClientRenamed e: + { + Clients = Clients.Rename(e.Id, e.Name); + + break; + } + + case AppClientRevoked e: + { + Clients = Clients.Revoke(e.Id); + + break; + } + + case AppWorkflowAdded e: + { + Workflows = Workflows.Add(e.WorkflowId, e.Name); + + break; + } + + case AppWorkflowUpdated e: + { + Workflows = Workflows.Update(e.WorkflowId, e.Workflow); + + break; + } + + case AppWorkflowDeleted e: + { + Workflows = Workflows.Remove(e.WorkflowId); + + break; + } + + case AppPatternAdded e: + { + Patterns = Patterns.Add(e.PatternId, e.Name, e.Pattern, e.Message); + + break; + } + + case AppPatternDeleted e: + { + Patterns = Patterns.Remove(e.PatternId); + + break; + } + + case AppPatternUpdated e: + { + Patterns = Patterns.Update(e.PatternId, e.Name, e.Pattern, e.Message); + + break; + } + + case AppRoleAdded e: + { + Roles = Roles.Add(e.Name); + + break; + } + + case AppRoleDeleted e: + { + Roles = Roles.Remove(e.Name); + + break; + } + + case AppRoleUpdated e: + { + Roles = Roles.Update(e.Name, e.Permissions); + + break; + } + + case AppLanguageAdded e: + { + LanguagesConfig = LanguagesConfig.Set(e.Language); + + break; + } + + case AppLanguageRemoved e: + { + LanguagesConfig = LanguagesConfig.Remove(e.Language); + + break; + } + + case AppLanguageUpdated e: + { + LanguagesConfig = LanguagesConfig.Set(e.Language, e.IsOptional, e.Fallback); + + if (e.IsMaster) + { + LanguagesConfig = LanguagesConfig.MakeMaster(e.Language); + } + + break; + } + + case AppArchived _: + { + Plan = null; + + IsArchived = true; + + break; + } + } + } + + public override AppState Apply(Envelope @event) + { + return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/AlwaysCreateClientCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/AlwaysCreateClientCommandMiddleware.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/AlwaysCreateClientCommandMiddleware.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/AlwaysCreateClientCommandMiddleware.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/AssetFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/AssetFieldBuilder.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/AssetFieldBuilder.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/AssetFieldBuilder.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/BooleanFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/BooleanFieldBuilder.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/BooleanFieldBuilder.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/BooleanFieldBuilder.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/DateTimeFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/DateTimeFieldBuilder.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/DateTimeFieldBuilder.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/DateTimeFieldBuilder.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs new file mode 100644 index 000000000..31fdbf82a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; + +namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders +{ + public abstract class FieldBuilder + { + private readonly UpsertSchemaField field; + + protected T Properties() where T : FieldProperties + { + return (T)field.Properties; + } + + protected FieldBuilder(UpsertSchemaField field) + { + this.field = field; + } + + public FieldBuilder Label(string? label) + { + field.Properties.Label = label; + + return this; + } + + public FieldBuilder Hints(string? hints) + { + field.Properties.Hints = hints; + + return this; + } + + public FieldBuilder Localizable() + { + field.Partitioning = Partitioning.Language.Key; + + return this; + } + + public FieldBuilder Disabled() + { + field.IsDisabled = true; + + return this; + } + + public FieldBuilder Required() + { + field.Properties.IsRequired = true; + + return this; + } + + public FieldBuilder ShowInList() + { + field.Properties.IsListField = true; + + return this; + } + + public FieldBuilder ShowInReferences() + { + field.Properties.IsReferenceField = true; + + return this; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/JsonFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/JsonFieldBuilder.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/JsonFieldBuilder.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/JsonFieldBuilder.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/NumberFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/NumberFieldBuilder.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/NumberFieldBuilder.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/NumberFieldBuilder.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs new file mode 100644 index 000000000..8ee0eea85 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs @@ -0,0 +1,149 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders +{ + public sealed class SchemaBuilder + { + private readonly CreateSchema command; + + public SchemaBuilder(CreateSchema command) + { + this.command = command; + } + + public static SchemaBuilder Create(string name) + { + var schemaName = name.ToKebabCase(); + + return new SchemaBuilder(new CreateSchema + { + Name = schemaName + }).Published().WithLabel(name); + } + + public SchemaBuilder WithLabel(string? label) + { + command.Properties ??= new SchemaProperties(); + command.Properties.Label = label; + + return this; + } + + public SchemaBuilder WithScripts(SchemaScripts scripts) + { + command.Scripts = scripts; + + return this; + } + + public SchemaBuilder Published() + { + command.IsPublished = true; + + return this; + } + + public SchemaBuilder Singleton() + { + command.IsSingleton = true; + + return this; + } + + public SchemaBuilder AddAssets(string name, Action configure) + { + var field = AddField(name); + + configure(new AssetFieldBuilder(field)); + + return this; + } + + public SchemaBuilder AddBoolean(string name, Action configure) + { + var field = AddField(name); + + configure(new BooleanFieldBuilder(field)); + + return this; + } + + public SchemaBuilder AddDateTime(string name, Action configure) + { + var field = AddField(name); + + configure(new DateTimeFieldBuilder(field)); + + return this; + } + + public SchemaBuilder AddJson(string name, Action configure) + { + var field = AddField(name); + + configure(new JsonFieldBuilder(field)); + + return this; + } + + public SchemaBuilder AddNumber(string name, Action configure) + { + var field = AddField(name); + + configure(new NumberFieldBuilder(field)); + + return this; + } + + public SchemaBuilder AddString(string name, Action configure) + { + var field = AddField(name); + + configure(new StringFieldBuilder(field)); + + return this; + } + + public SchemaBuilder AddTags(string name, Action configure) + { + var field = AddField(name); + + configure(new TagsFieldBuilder(field)); + + return this; + } + + private UpsertSchemaField AddField(string name) where T : FieldProperties, new() + { + var field = new UpsertSchemaField + { + Name = name.ToCamelCase(), + Properties = new T + { + Label = name + } + }; + + command.Fields ??= new List(); + command.Fields.Add(field); + + return field; + } + + public CreateSchema Build() + { + return command; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs new file mode 100644 index 000000000..75ce75746 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs @@ -0,0 +1,59 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure.Collections; + +namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders +{ + public class StringFieldBuilder : FieldBuilder + { + public StringFieldBuilder(UpsertSchemaField field) + : base(field) + { + } + + public StringFieldBuilder AsTextArea() + { + Properties().Editor = StringFieldEditor.TextArea; + + return this; + } + + public StringFieldBuilder AsRichText() + { + Properties().Editor = StringFieldEditor.RichText; + + return this; + } + + public StringFieldBuilder AsDropDown(params string[] values) + { + Properties().AllowedValues = ReadOnlyCollection.Create(values); + Properties().Editor = StringFieldEditor.Dropdown; + + return this; + } + + public StringFieldBuilder Pattern(string pattern, string? message = null) + { + Properties().Pattern = pattern; + Properties().PatternMessage = message; + + return this; + } + + public StringFieldBuilder Length(int maxLength, int minLength = 0) + { + Properties().MaxLength = maxLength; + Properties().MinLength = minLength; + + return this; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfileCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfileCommandMiddleware.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfileCommandMiddleware.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfileCommandMiddleware.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/DefaultScripts.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/DefaultScripts.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/DefaultScripts.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/DefaultScripts.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs new file mode 100644 index 000000000..7af666c55 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetChangedTriggerHandler : RuleTriggerHandler + { + private readonly IScriptEngine scriptEngine; + private readonly IAssetLoader assetLoader; + + public AssetChangedTriggerHandler(IScriptEngine scriptEngine, IAssetLoader assetLoader) + { + Guard.NotNull(scriptEngine); + Guard.NotNull(assetLoader); + + this.scriptEngine = scriptEngine; + + this.assetLoader = assetLoader; + } + + protected override async Task CreateEnrichedEventAsync(Envelope @event) + { + var result = new EnrichedAssetEvent(); + + var asset = await assetLoader.GetAsync(@event.Payload.AssetId, @event.Headers.EventStreamNumber()); + + SimpleMapper.Map(asset, result); + + switch (@event.Payload) + { + case AssetCreated _: + result.Type = EnrichedAssetEventType.Created; + break; + case AssetAnnotated _: + result.Type = EnrichedAssetEventType.Annotated; + break; + case AssetUpdated _: + result.Type = EnrichedAssetEventType.Updated; + break; + case AssetDeleted _: + result.Type = EnrichedAssetEventType.Deleted; + break; + } + + result.Name = $"Asset{result.Type}"; + + return result; + } + + protected override bool Trigger(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger) + { + return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs new file mode 100644 index 000000000..0a91fd30a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -0,0 +1,175 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetCommandMiddleware : GrainCommandMiddleware + { + private readonly IAssetStore assetStore; + private readonly IAssetEnricher assetEnricher; + private readonly IAssetQueryService assetQuery; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly IContextProvider contextProvider; + private readonly IEnumerable> tagGenerators; + + public AssetCommandMiddleware( + IGrainFactory grainFactory, + IAssetEnricher assetEnricher, + IAssetQueryService assetQuery, + IAssetStore assetStore, + IAssetThumbnailGenerator assetThumbnailGenerator, + IContextProvider contextProvider, + IEnumerable> tagGenerators) + : base(grainFactory) + { + Guard.NotNull(assetEnricher); + Guard.NotNull(assetStore); + Guard.NotNull(assetQuery); + Guard.NotNull(assetThumbnailGenerator); + Guard.NotNull(contextProvider); + Guard.NotNull(tagGenerators); + + this.assetStore = assetStore; + this.assetEnricher = assetEnricher; + this.assetQuery = assetQuery; + this.assetThumbnailGenerator = assetThumbnailGenerator; + this.contextProvider = contextProvider; + this.tagGenerators = tagGenerators; + } + + public override async Task HandleAsync(CommandContext context, Func next) + { + var tempFile = context.ContextId.ToString(); + + switch (context.Command) + { + case CreateAsset createAsset: + { + await EnrichWithImageInfosAsync(createAsset); + await EnrichWithHashAndUploadAsync(createAsset, tempFile); + + try + { + var ctx = contextProvider.Context.Clone().WithNoAssetEnrichment(); + + var existings = await assetQuery.QueryByHashAsync(ctx, createAsset.AppId.Id, createAsset.FileHash); + + foreach (var existing in existings) + { + if (IsDuplicate(existing, createAsset.File)) + { + var result = new AssetCreatedResult(existing, true); + + context.Complete(result); + + await next(); + return; + } + } + + GenerateTags(createAsset); + + await HandleCoreAsync(context, next); + + var asset = context.Result(); + + context.Complete(new AssetCreatedResult(asset, false)); + + await assetStore.CopyAsync(tempFile, createAsset.AssetId.ToString(), asset.FileVersion, null); + } + finally + { + await assetStore.DeleteAsync(tempFile); + } + + break; + } + + case UpdateAsset updateAsset: + { + await EnrichWithImageInfosAsync(updateAsset); + await EnrichWithHashAndUploadAsync(updateAsset, tempFile); + + try + { + await HandleCoreAsync(context, next); + + var asset = context.Result(); + + await assetStore.CopyAsync(tempFile, updateAsset.AssetId.ToString(), asset.FileVersion, null); + } + finally + { + await assetStore.DeleteAsync(tempFile); + } + + break; + } + + default: + await HandleCoreAsync(context, next); + break; + } + } + + private async Task HandleCoreAsync(CommandContext context, Func next) + { + await base.HandleAsync(context, next); + + if (context.PlainResult is IAssetEntity asset && !(context.PlainResult is IEnrichedAssetEntity)) + { + var enriched = await assetEnricher.EnrichAsync(asset, contextProvider.Context); + + context.Complete(enriched); + } + } + + private static bool IsDuplicate(IAssetEntity asset, AssetFile file) + { + return asset?.FileName == file.FileName && asset.FileSize == file.FileSize; + } + + private async Task EnrichWithImageInfosAsync(UploadAssetCommand command) + { + command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); + } + + private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile) + { + using (var hashStream = new HasherStream(command.File.OpenRead(), HashAlgorithmName.SHA256)) + { + await assetStore.UploadAsync(tempFile, hashStream); + + command.FileHash = $"{hashStream.GetHashStringAndReset()}{command.File.FileName}{command.File.FileSize}".Sha256Base64(); + } + } + + private void GenerateTags(CreateAsset createAsset) + { + if (createAsset.Tags == null) + { + createAsset.Tags = new HashSet(); + } + + foreach (var tagGenerator in tagGenerators) + { + tagGenerator.GenerateTags(createAsset, createAsset.Tags); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs new file mode 100644 index 000000000..7b31dbfcc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs @@ -0,0 +1,183 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.Guards; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetGrain : LogSnapshotDomainObjectGrain, IAssetGrain + { + private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); + private readonly ITagService tagService; + + public AssetGrain(IStore store, ITagService tagService, IActivationLimit limit, ISemanticLog log) + : base(store, log) + { + Guard.NotNull(tagService); + + this.tagService = tagService; + + limit?.SetLimit(5000, Lifetime); + } + + protected override Task OnActivateAsync(Guid key) + { + TryDelayDeactivation(Lifetime); + + return base.OnActivateAsync(key); + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + VerifyNotDeleted(); + + switch (command) + { + case CreateAsset createAsset: + return CreateReturnAsync(createAsset, async c => + { + GuardAsset.CanCreate(c); + + var tagIds = await NormalizeTagsAsync(c.AppId.Id, c.Tags); + + Create(c, tagIds); + + return Snapshot; + }); + case UpdateAsset updateAsset: + return UpdateReturn(updateAsset, c => + { + GuardAsset.CanUpdate(c); + + Update(c); + + return Snapshot; + }); + case AnnotateAsset annotateAsset: + return UpdateReturnAsync(annotateAsset, async c => + { + GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug); + + var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags); + + Annotate(c, tagIds); + + return Snapshot; + }); + case DeleteAsset deleteAsset: + return UpdateAsync(deleteAsset, async c => + { + GuardAsset.CanDelete(c); + + await tagService.NormalizeTagsAsync(Snapshot.AppId.Id, TagGroups.Assets, null, Snapshot.Tags); + + Delete(c); + }); + default: + throw new NotSupportedException(); + } + } + + private async Task?> NormalizeTagsAsync(Guid appId, HashSet tags) + { + if (tags == null) + { + return null; + } + + var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Assets, tags, Snapshot.Tags); + + return new HashSet(normalized.Values); + } + + public void Create(CreateAsset command, HashSet? tagIds) + { + var @event = SimpleMapper.Map(command, new AssetCreated + { + IsImage = command.ImageInfo != null, + FileName = command.File.FileName, + FileSize = command.File.FileSize, + FileVersion = 0, + MimeType = command.File.MimeType, + PixelWidth = command.ImageInfo?.PixelWidth, + PixelHeight = command.ImageInfo?.PixelHeight, + Slug = command.File.FileName.ToAssetSlug() + }); + + @event.Tags = tagIds; + + RaiseEvent(@event); + } + + public void Update(UpdateAsset command) + { + var @event = SimpleMapper.Map(command, new AssetUpdated + { + FileVersion = Snapshot.FileVersion + 1, + FileSize = command.File.FileSize, + MimeType = command.File.MimeType, + PixelWidth = command.ImageInfo?.PixelWidth, + PixelHeight = command.ImageInfo?.PixelHeight, + IsImage = command.ImageInfo != null + }); + + RaiseEvent(@event); + } + + public void Annotate(AnnotateAsset command, HashSet? tagIds) + { + var @event = SimpleMapper.Map(command, new AssetAnnotated()); + + @event.Tags = tagIds; + + RaiseEvent(@event); + } + + public void Delete(DeleteAsset command) + { + RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize })); + } + + private void RaiseEvent(AppEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + RaiseEvent(Envelope.Create(@event)); + } + + private void VerifyNotDeleted() + { + if (Snapshot.IsDeleted) + { + throw new DomainException("Asset has already been deleted"); + } + } + + public Task> GetStateAsync(long version = EtagVersion.Any) + { + return J.AsTask(GetSnapshot(version)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs new file mode 100644 index 000000000..c34fc9dc9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.UsageTracking; + +#pragma warning disable CS0649 + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public partial class AssetUsageTracker : IAssetUsageTracker, IEventConsumer + { + private const string Category = "Default"; + private const string CounterTotalCount = "TotalAssets"; + private const string CounterTotalSize = "TotalSize"; + private static readonly DateTime SummaryDate; + private readonly IUsageRepository usageStore; + + public AssetUsageTracker(IUsageRepository usageStore) + { + Guard.NotNull(usageStore); + + this.usageStore = usageStore; + } + + public async Task GetTotalSizeAsync(Guid appId) + { + var key = GetKey(appId); + + var entries = await usageStore.QueryAsync(key, SummaryDate, SummaryDate); + + return (long)entries.Select(x => x.Counters.Get(CounterTotalSize)).FirstOrDefault(); + } + + public async Task> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate) + { + var enriched = new List(); + + var usagesFlat = await usageStore.QueryAsync(GetKey(appId), fromDate, toDate); + + for (var date = fromDate; date <= toDate; date = date.AddDays(1)) + { + var stored = usagesFlat.FirstOrDefault(x => x.Date == date && x.Category == Category); + + var totalCount = 0L; + var totalSize = 0L; + + if (stored != null) + { + totalCount = (long)stored.Counters.Get(CounterTotalCount); + totalSize = (long)stored.Counters.Get(CounterTotalSize); + } + + enriched.Add(new AssetStats(date, totalCount, totalSize)); + } + + return enriched; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs new file mode 100644 index 000000000..aa572fa9c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs @@ -0,0 +1,127 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class BackupAssets : BackupHandlerWithStore + { + private const string TagsFile = "AssetTags.json"; + private readonly HashSet assetIds = new HashSet(); + private readonly IAssetStore assetStore; + private readonly ITagService tagService; + + public override string Name { get; } = "Assets"; + + public BackupAssets(IStore store, IAssetStore assetStore, ITagService tagService) + : base(store) + { + Guard.NotNull(assetStore); + Guard.NotNull(tagService); + + this.assetStore = assetStore; + + this.tagService = tagService; + } + + public override Task BackupAsync(Guid appId, BackupWriter writer) + { + return BackupTagsAsync(appId, writer); + } + + public override Task BackupEventAsync(Envelope @event, Guid appId, BackupWriter writer) + { + switch (@event.Payload) + { + case AssetCreated assetCreated: + return WriteAssetAsync(assetCreated.AssetId, assetCreated.FileVersion, writer); + case AssetUpdated assetUpdated: + return WriteAssetAsync(assetUpdated.AssetId, assetUpdated.FileVersion, writer); + } + + return TaskHelper.Done; + } + + public override async Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) + { + switch (@event.Payload) + { + case AssetCreated assetCreated: + await ReadAssetAsync(assetCreated.AssetId, assetCreated.FileVersion, reader); + break; + case AssetUpdated assetUpdated: + await ReadAssetAsync(assetUpdated.AssetId, assetUpdated.FileVersion, reader); + break; + } + + return true; + } + + public override async Task RestoreAsync(Guid appId, BackupReader reader) + { + await RestoreTagsAsync(appId, reader); + + await RebuildManyAsync(assetIds, RebuildAsync); + } + + private async Task RestoreTagsAsync(Guid appId, BackupReader reader) + { + var tags = await reader.ReadJsonAttachmentAsync(TagsFile); + + await tagService.RebuildTagsAsync(appId, TagGroups.Assets, tags); + } + + private async Task BackupTagsAsync(Guid appId, BackupWriter writer) + { + var tags = await tagService.GetExportableTagsAsync(appId, TagGroups.Assets); + + await writer.WriteJsonAsync(TagsFile, tags); + } + + private Task WriteAssetAsync(Guid assetId, long fileVersion, BackupWriter writer) + { + return writer.WriteBlobAsync(GetName(assetId, fileVersion), stream => + { + return assetStore.DownloadAsync(assetId.ToString(), fileVersion, null, stream); + }); + } + + private Task ReadAssetAsync(Guid assetId, long fileVersion, BackupReader reader) + { + assetIds.Add(assetId); + + return reader.ReadBlobAsync(GetName(reader.OldGuid(assetId), fileVersion), async stream => + { + try + { + await assetStore.UploadAsync(assetId.ToString(), fileVersion, null, stream, true); + } + catch (AssetAlreadyExistsException) + { + return; + } + }); + } + + private static string GetName(Guid assetId, long fileVersion) + { + return $"{assetId}_{fileVersion}.asset"; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs new file mode 100644 index 000000000..6a519931c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Assets; + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public abstract class UploadAssetCommand : AssetCommand + { + public AssetFile File { get; set; } + + public ImageInfo? ImageInfo { get; set; } + + public string FileHash { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs new file mode 100644 index 000000000..486083010 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Assets.Guards +{ + public static class GuardAsset + { + public static void CanAnnotate(AnnotateAsset command, string oldFileName, string oldSlug) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot rename asset.", e => + { + if (string.IsNullOrWhiteSpace(command.FileName) && + string.IsNullOrWhiteSpace(command.Slug) && + command.Tags == null) + { + e("Either file name, slug or tags must be defined.", nameof(command.FileName), nameof(command.Slug), nameof(command.Tags)); + } + + if (!string.IsNullOrWhiteSpace(command.FileName) && string.Equals(command.FileName, oldFileName)) + { + e(Not.New("Asset", "name"), nameof(command.FileName)); + } + + if (!string.IsNullOrWhiteSpace(command.Slug) && string.Equals(command.Slug, oldSlug)) + { + e(Not.New("Asset", "slug"), nameof(command.Slug)); + } + }); + } + + public static void CanCreate(CreateAsset command) + { + Guard.NotNull(command); + } + + public static void CanUpdate(UpdateAsset command) + { + Guard.NotNull(command); + } + + public static void CanDelete(DeleteAsset command) + { + Guard.NotNull(command); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs new file mode 100644 index 000000000..dc94e0293 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.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; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public interface IAssetQueryService + { + Task> QueryByHashAsync(Context context, Guid appId, string hash); + + Task> QueryAsync(Context context, Q query); + + Task FindAssetAsync(Context context, Guid id); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs new file mode 100644 index 000000000..78cdafee7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs @@ -0,0 +1,93 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public sealed class AssetEnricher : IAssetEnricher + { + private readonly ITagService tagService; + + public AssetEnricher(ITagService tagService) + { + Guard.NotNull(tagService); + + this.tagService = tagService; + } + + public async Task EnrichAsync(IAssetEntity asset, Context context) + { + Guard.NotNull(asset); + Guard.NotNull(context); + + var enriched = await EnrichAsync(Enumerable.Repeat(asset, 1), context); + + return enriched[0]; + } + + public async Task> EnrichAsync(IEnumerable assets, Context context) + { + Guard.NotNull(assets); + Guard.NotNull(context); + + using (Profiler.TraceMethod()) + { + var results = assets.Select(x => SimpleMapper.Map(x, new AssetEntity())).ToList(); + + if (ShouldEnrich(context)) + { + await EnrichTagsAsync(results); + } + + return results; + } + } + + private async Task EnrichTagsAsync(List assets) + { + foreach (var group in assets.GroupBy(x => x.AppId.Id)) + { + var tagsById = await CalculateTags(group); + + foreach (var asset in group) + { + asset.TagNames = new HashSet(); + + if (asset.Tags != null) + { + foreach (var id in asset.Tags) + { + if (tagsById.TryGetValue(id, out var name)) + { + asset.TagNames.Add(name); + } + } + } + } + } + } + + private async Task> CalculateTags(IGrouping group) + { + var uniqueIds = group.Where(x => x.Tags != null).SelectMany(x => x.Tags).ToHashSet(); + + return await tagService.DenormalizeTagsAsync(group.Key, TagGroups.Assets, uniqueIds); + } + + private static bool ShouldEnrich(Context context) + { + return !context.IsNoAssetEnrichment(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs new file mode 100644 index 000000000..5265ae1fc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public sealed class AssetLoader : IAssetLoader + { + private readonly IGrainFactory grainFactory; + + public AssetLoader(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public async Task GetAsync(Guid id, long version) + { + using (Profiler.TraceMethod()) + { + var grain = grainFactory.GetGrain(id); + + var content = await grain.GetStateAsync(version); + + if (content.Value == null || content.Value.Version != version) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(IAssetEntity)); + } + + return content.Value; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs new file mode 100644 index 000000000..f91b8dd8d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs @@ -0,0 +1,174 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Options; +using Microsoft.OData; +using Microsoft.OData.Edm; +using NJsonSchema; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.Queries.OData; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public class AssetQueryParser + { + private readonly JsonSchema jsonSchema = BuildJsonSchema(); + private readonly IEdmModel edmModel = BuildEdmModel(); + private readonly IJsonSerializer jsonSerializer; + private readonly ITagService tagService; + private readonly AssetOptions options; + + public AssetQueryParser(IJsonSerializer jsonSerializer, ITagService tagService, IOptions options) + { + Guard.NotNull(jsonSerializer); + Guard.NotNull(options); + Guard.NotNull(tagService); + + this.jsonSerializer = jsonSerializer; + this.options = options.Value; + this.tagService = tagService; + } + + public virtual ClrQuery ParseQuery(Context context, Q q) + { + Guard.NotNull(context); + + using (Profiler.TraceMethod()) + { + var result = new ClrQuery(); + + if (!string.IsNullOrWhiteSpace(q?.JsonQuery)) + { + result = ParseJson(q.JsonQuery); + } + else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) + { + result = ParseOData(q.ODataQuery); + } + + if (result.Filter != null) + { + result.Filter = FilterTagTransformer.Transform(result.Filter, context.App.Id, tagService); + } + + if (result.Sort.Count == 0) + { + result.Sort.Add(new SortNode(new List { "lastModified" }, SortOrder.Descending)); + } + + if (result.Take == long.MaxValue) + { + result.Take = options.DefaultPageSize; + } + else if (result.Take > options.MaxResults) + { + result.Take = options.MaxResults; + } + + return result; + } + } + + private ClrQuery ParseJson(string json) + { + return jsonSchema.Parse(json, jsonSerializer); + } + + private ClrQuery ParseOData(string odata) + { + try + { + return edmModel.ParseQuery(odata).ToQuery(); + } + catch (NotSupportedException) + { + throw new ValidationException("OData operation is not supported."); + } + catch (ODataException ex) + { + throw new ValidationException($"Failed to parse query: {ex.Message}", ex); + } + } + + private static JsonSchema BuildJsonSchema() + { + var schema = new JsonSchema { Title = "Asset", Type = JsonObjectType.Object }; + + void AddProperty(string name, JsonObjectType type, string? format = null) + { + var property = new JsonSchemaProperty { Type = type, Format = format }; + + schema.Properties[name.ToCamelCase()] = property; + } + + AddProperty(nameof(IAssetEntity.Id), JsonObjectType.String, JsonFormatStrings.Guid); + AddProperty(nameof(IAssetEntity.Created), JsonObjectType.String, JsonFormatStrings.DateTime); + AddProperty(nameof(IAssetEntity.CreatedBy), JsonObjectType.String); + AddProperty(nameof(IAssetEntity.LastModified), JsonObjectType.String, JsonFormatStrings.DateTime); + AddProperty(nameof(IAssetEntity.LastModifiedBy), JsonObjectType.String); + AddProperty(nameof(IAssetEntity.Version), JsonObjectType.Integer); + AddProperty(nameof(IAssetEntity.FileName), JsonObjectType.String); + AddProperty(nameof(IAssetEntity.FileHash), JsonObjectType.String); + AddProperty(nameof(IAssetEntity.FileSize), JsonObjectType.Integer); + AddProperty(nameof(IAssetEntity.FileVersion), JsonObjectType.Integer); + AddProperty(nameof(IAssetEntity.IsImage), JsonObjectType.Boolean); + AddProperty(nameof(IAssetEntity.MimeType), JsonObjectType.String); + AddProperty(nameof(IAssetEntity.PixelHeight), JsonObjectType.Integer); + AddProperty(nameof(IAssetEntity.PixelWidth), JsonObjectType.Integer); + AddProperty(nameof(IAssetEntity.Slug), JsonObjectType.String); + AddProperty(nameof(IAssetEntity.Tags), JsonObjectType.String); + + return schema; + } + + private static IEdmModel BuildEdmModel() + { + var entityType = new EdmEntityType("Squidex", "Asset"); + + void AddProperty(string name, EdmPrimitiveTypeKind type) + { + entityType.AddStructuralProperty(name.ToCamelCase(), type); + } + + AddProperty(nameof(IAssetEntity.Id), EdmPrimitiveTypeKind.String); + AddProperty(nameof(IAssetEntity.Created), EdmPrimitiveTypeKind.DateTimeOffset); + AddProperty(nameof(IAssetEntity.CreatedBy), EdmPrimitiveTypeKind.String); + AddProperty(nameof(IAssetEntity.LastModified), EdmPrimitiveTypeKind.DateTimeOffset); + AddProperty(nameof(IAssetEntity.LastModifiedBy), EdmPrimitiveTypeKind.String); + AddProperty(nameof(IAssetEntity.Version), EdmPrimitiveTypeKind.Int64); + AddProperty(nameof(IAssetEntity.FileName), EdmPrimitiveTypeKind.String); + AddProperty(nameof(IAssetEntity.FileHash), EdmPrimitiveTypeKind.String); + AddProperty(nameof(IAssetEntity.FileSize), EdmPrimitiveTypeKind.Int64); + AddProperty(nameof(IAssetEntity.FileVersion), EdmPrimitiveTypeKind.Int64); + AddProperty(nameof(IAssetEntity.IsImage), EdmPrimitiveTypeKind.Boolean); + AddProperty(nameof(IAssetEntity.MimeType), EdmPrimitiveTypeKind.String); + AddProperty(nameof(IAssetEntity.PixelHeight), EdmPrimitiveTypeKind.Int32); + AddProperty(nameof(IAssetEntity.PixelWidth), EdmPrimitiveTypeKind.Int32); + AddProperty(nameof(IAssetEntity.Slug), EdmPrimitiveTypeKind.String); + AddProperty(nameof(IAssetEntity.Tags), EdmPrimitiveTypeKind.String); + + var container = new EdmEntityContainer("Squidex", "Container"); + + container.AddEntitySet("AssetSet", entityType); + + var model = new EdmModel(); + + model.AddElement(container); + model.AddElement(entityType); + + return model; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs new file mode 100644 index 000000000..098b5a516 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs @@ -0,0 +1,97 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public sealed class AssetQueryService : IAssetQueryService + { + private readonly IAssetEnricher assetEnricher; + private readonly IAssetRepository assetRepository; + private readonly AssetQueryParser queryParser; + + public AssetQueryService( + IAssetEnricher assetEnricher, + IAssetRepository assetRepository, + AssetQueryParser queryParser) + { + Guard.NotNull(assetEnricher); + Guard.NotNull(assetRepository); + Guard.NotNull(queryParser); + + this.assetEnricher = assetEnricher; + this.assetRepository = assetRepository; + this.queryParser = queryParser; + } + + public async Task FindAssetAsync(Context context, Guid id) + { + var asset = await assetRepository.FindAssetAsync(id); + + if (asset != null) + { + return await assetEnricher.EnrichAsync(asset, context); + } + + return null; + } + + public async Task> QueryByHashAsync(Context context, Guid appId, string hash) + { + Guard.NotNull(hash); + + var assets = await assetRepository.QueryByHashAsync(appId, hash); + + return await assetEnricher.EnrichAsync(assets, context); + } + + public async Task> QueryAsync(Context context, Q query) + { + Guard.NotNull(context); + Guard.NotNull(query); + + IResultList assets; + + if (query.Ids != null && query.Ids.Count > 0) + { + assets = await QueryByIdsAsync(context, query); + } + else + { + assets = await QueryByQueryAsync(context, query); + } + + var enriched = await assetEnricher.EnrichAsync(assets, context); + + return ResultList.Create(assets.Total, enriched); + } + + private async Task> QueryByQueryAsync(Context context, Q query) + { + var parsedQuery = queryParser.ParseQuery(context, query); + + return await assetRepository.QueryAsync(context.App.Id, parsedQuery); + } + + private async Task> QueryByIdsAsync(Context context, Q query) + { + var assets = await assetRepository.QueryAsync(context.App.Id, new HashSet(query.Ids)); + + return Sort(assets, query.Ids); + } + + private static IResultList Sort(IResultList assets, IReadOnlyList ids) + { + return assets.SortSet(x => x.Id, ids); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs new file mode 100644 index 000000000..d39af6a13 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// 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.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public sealed class FilterTagTransformer : TransformVisitor + { + private readonly ITagService tagService; + private readonly Guid appId; + + private FilterTagTransformer(Guid appId, ITagService tagService) + { + this.appId = appId; + + this.tagService = tagService; + } + + public static FilterNode? Transform(FilterNode nodeIn, Guid appId, ITagService tagService) + { + Guard.NotNull(nodeIn); + Guard.NotNull(tagService); + + return nodeIn.Accept(new FilterTagTransformer(appId, tagService)); + } + + public override FilterNode? Visit(CompareFilter nodeIn) + { + if (string.Equals(nodeIn.Path[0], nameof(IAssetEntity.Tags), StringComparison.OrdinalIgnoreCase) && nodeIn.Value.Value is string stringValue) + { + var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, HashSet.Of(stringValue))).Result; + + if (tagNames.TryGetValue(stringValue, out var normalized)) + { + return new CompareFilter(nodeIn.Path, nodeIn.Operator, normalized); + } + } + + return nodeIn; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs new file mode 100644 index 000000000..11a45e228 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.Assets.Repositories +{ + public interface IAssetRepository + { + Task> QueryByHashAsync(Guid appId, string hash); + + Task> QueryAsync(Guid appId, ClrQuery query); + + Task> QueryAsync(Guid appId, HashSet ids); + + Task FindAssetAsync(Guid id, bool allowDeleted = false); + + Task FindAssetBySlugAsync(Guid appId, string slug); + + Task RemoveAsync(Guid appId); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs new file mode 100644 index 000000000..074b964a7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs @@ -0,0 +1,262 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Orleans.Concurrency; +using Squidex.Domain.Apps.Entities.Backup.Helpers; +using Squidex.Domain.Apps.Entities.Backup.State; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + [Reentrant] + public sealed class BackupGrain : GrainOfGuid, IBackupGrain + { + private const int MaxBackups = 10; + private static readonly Duration UpdateDuration = Duration.FromSeconds(1); + private readonly IAssetStore assetStore; + private readonly IBackupArchiveLocation backupArchiveLocation; + private readonly IClock clock; + private readonly IJsonSerializer serializer; + private readonly IServiceProvider serviceProvider; + private readonly IEventDataFormatter eventDataFormatter; + private readonly IEventStore eventStore; + private readonly ISemanticLog log; + private readonly IGrainState state; + private CancellationTokenSource? currentTask; + private BackupStateJob? currentJob; + + public BackupGrain( + IAssetStore assetStore, + IBackupArchiveLocation backupArchiveLocation, + IClock clock, + IEventStore eventStore, + IEventDataFormatter eventDataFormatter, + IJsonSerializer serializer, + IServiceProvider serviceProvider, + ISemanticLog log, + IGrainState state) + { + Guard.NotNull(assetStore); + Guard.NotNull(backupArchiveLocation); + Guard.NotNull(clock); + Guard.NotNull(eventStore); + Guard.NotNull(eventDataFormatter); + Guard.NotNull(serviceProvider); + Guard.NotNull(serializer); + Guard.NotNull(state); + Guard.NotNull(log); + + this.assetStore = assetStore; + this.backupArchiveLocation = backupArchiveLocation; + this.clock = clock; + this.eventStore = eventStore; + this.eventDataFormatter = eventDataFormatter; + this.serializer = serializer; + this.serviceProvider = serviceProvider; + this.state = state; + this.log = log; + } + + protected override Task OnActivateAsync(Guid key) + { + RecoverAfterRestartAsync().Forget(); + + return TaskHelper.Done; + } + + private async Task RecoverAfterRestartAsync() + { + foreach (var job in state.Value.Jobs) + { + if (!job.Stopped.HasValue) + { + var jobId = job.Id.ToString(); + + job.Stopped = clock.GetCurrentInstant(); + + await Safe.DeleteAsync(backupArchiveLocation, jobId, log); + await Safe.DeleteAsync(assetStore, jobId, log); + + job.Status = JobStatus.Failed; + + await state.WriteAsync(); + } + } + } + + public async Task RunAsync() + { + if (currentTask != null) + { + throw new DomainException("Another backup process is already running."); + } + + if (state.Value.Jobs.Count >= MaxBackups) + { + throw new DomainException($"You cannot have more than {MaxBackups} backups."); + } + + var job = new BackupStateJob + { + Id = Guid.NewGuid(), + Started = clock.GetCurrentInstant(), + Status = JobStatus.Started + }; + + currentTask = new CancellationTokenSource(); + currentJob = job; + + state.Value.Jobs.Insert(0, job); + + await state.WriteAsync(); + + Process(job, currentTask.Token); + } + + private void Process(BackupStateJob job, CancellationToken ct) + { + ProcessAsync(job, ct).Forget(); + } + + private async Task ProcessAsync(BackupStateJob job, CancellationToken ct) + { + var jobId = job.Id.ToString(); + + var handlers = CreateHandlers(); + + var lastTimestamp = job.Started; + + try + { + using (var stream = await backupArchiveLocation.OpenStreamAsync(jobId)) + { + using (var writer = new BackupWriter(serializer, stream, true)) + { + await eventStore.QueryAsync(async storedEvent => + { + var @event = eventDataFormatter.Parse(storedEvent.Data); + + writer.WriteEvent(storedEvent); + + foreach (var handler in handlers) + { + await handler.BackupEventAsync(@event, Key, writer); + } + + job.HandledEvents = writer.WrittenEvents; + job.HandledAssets = writer.WrittenAttachments; + + lastTimestamp = await WritePeriodically(lastTimestamp); + }, SquidexHeaders.AppId, Key.ToString(), null, ct); + + foreach (var handler in handlers) + { + await handler.BackupAsync(Key, writer); + } + + foreach (var handler in handlers) + { + await handler.CompleteBackupAsync(Key, writer); + } + } + + stream.Position = 0; + + ct.ThrowIfCancellationRequested(); + + await assetStore.UploadAsync(jobId, 0, null, stream, false, ct); + } + + job.Status = JobStatus.Completed; + } + catch (Exception ex) + { + log.LogError(ex, jobId, (ctx, w) => w + .WriteProperty("action", "makeBackup") + .WriteProperty("status", "failed") + .WriteProperty("backupId", ctx)); + + job.Status = JobStatus.Failed; + } + finally + { + await Safe.DeleteAsync(backupArchiveLocation, jobId, log); + + job.Stopped = clock.GetCurrentInstant(); + + await state.WriteAsync(); + + currentTask = null; + currentJob = null; + } + } + + private async Task WritePeriodically(Instant lastTimestamp) + { + var now = clock.GetCurrentInstant(); + + if ((now - lastTimestamp) >= UpdateDuration) + { + lastTimestamp = now; + + await state.WriteAsync(); + } + + return lastTimestamp; + } + + public async Task DeleteAsync(Guid id) + { + var job = state.Value.Jobs.FirstOrDefault(x => x.Id == id); + + if (job == null) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(IBackupJob)); + } + + if (currentJob == job) + { + currentTask?.Cancel(); + } + else + { + var jobId = job.Id.ToString(); + + await Safe.DeleteAsync(backupArchiveLocation, jobId, log); + await Safe.DeleteAsync(assetStore, jobId, log); + + state.Value.Jobs.Remove(job); + + await state.WriteAsync(); + } + } + + private IEnumerable CreateHandlers() + { + return serviceProvider.GetRequiredService>(); + } + + public Task>> GetStateAsync() + { + return J.AsTask(state.Value.Jobs.OfType().ToList()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupHandler.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/BackupHandler.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/BackupHandler.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs new file mode 100644 index 000000000..e64b767fe --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public abstract class BackupHandlerWithStore : BackupHandler + { + private readonly IStore store; + + protected BackupHandlerWithStore(IStore store) + { + Guard.NotNull(store); + + this.store = store; + } + + protected async Task RebuildManyAsync(IEnumerable ids, Func action) + { + foreach (var id in ids) + { + await action(id); + } + } + + protected async Task RebuildAsync(Guid key) where TState : IDomainState, new() + { + var state = new TState + { + Version = EtagVersion.Empty + }; + + var persistence = store.WithSnapshotsAndEventSourcing(typeof(TGrain), key, (TState s) => state = s, e => + { + state = state.Apply(e); + + state.Version++; + }); + + await persistence.ReadAsync(); + await persistence.WriteSnapshotAsync(state); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs new file mode 100644 index 000000000..da249a082 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs @@ -0,0 +1,155 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Backup.Helpers; +using Squidex.Domain.Apps.Entities.Backup.Model; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.States; + +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public sealed class BackupReader : DisposableObjectBase + { + private readonly GuidMapper guidMapper = new GuidMapper(); + private readonly ZipArchive archive; + private readonly IJsonSerializer serializer; + private int readEvents; + private int readAttachments; + + public int ReadEvents + { + get { return readEvents; } + } + + public int ReadAttachments + { + get { return readAttachments; } + } + + public BackupReader(IJsonSerializer serializer, Stream stream) + { + Guard.NotNull(serializer); + + this.serializer = serializer; + + archive = new ZipArchive(stream, ZipArchiveMode.Read, false); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + archive.Dispose(); + } + } + + public Guid OldGuid(Guid newId) + { + return guidMapper.OldGuid(newId); + } + + public Task ReadJsonAttachmentAsync(string name) + { + Guard.NotNullOrEmpty(name); + + var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); + + if (attachmentEntry == null) + { + throw new FileNotFoundException("Cannot find attachment.", name); + } + + T result; + + using (var stream = attachmentEntry.Open()) + { + result = serializer.Deserialize(stream, null, guidMapper.NewGuidOrValue); + } + + readAttachments++; + + return Task.FromResult(result); + } + + public async Task ReadBlobAsync(string name, Func handler) + { + Guard.NotNullOrEmpty(name); + Guard.NotNull(handler); + + var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); + + if (attachmentEntry == null) + { + throw new FileNotFoundException("Cannot find attachment.", name); + } + + using (var stream = attachmentEntry.Open()) + { + await handler(stream); + } + + readAttachments++; + } + + public async Task ReadEventsAsync(IStreamNameResolver streamNameResolver, IEventDataFormatter formatter, Func<(string Stream, Envelope Event), Task> handler) + { + Guard.NotNull(handler); + Guard.NotNull(formatter); + Guard.NotNull(streamNameResolver); + + while (true) + { + var eventEntry = archive.GetEntry(ArchiveHelper.GetEventPath(readEvents)); + + if (eventEntry == null) + { + break; + } + + using (var stream = eventEntry.Open()) + { + var (streamName, data) = serializer.Deserialize(stream).ToEvent(); + + MapHeaders(data); + + var eventStream = streamNameResolver.WithNewId(streamName, guidMapper.NewGuidOrNull); + var eventEnvelope = formatter.Parse(data, guidMapper.NewGuidOrValue); + + await handler((eventStream, eventEnvelope)); + } + + readEvents++; + } + } + + private void MapHeaders(EventData data) + { + foreach (var kvp in data.Headers.ToList()) + { + if (kvp.Value.Type == JsonValueType.String) + { + var newGuid = guidMapper.NewGuidOrNull(kvp.Value.ToString()); + + if (newGuid != null) + { + data.Headers.Add(kvp.Key, newGuid); + } + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupRestoreException.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupRestoreException.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/BackupRestoreException.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/BackupRestoreException.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupVersion.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupVersion.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/BackupVersion.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/BackupVersion.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs new file mode 100644 index 000000000..217f88541 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.IO.Compression; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Backup.Helpers; +using Squidex.Domain.Apps.Entities.Backup.Model; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public sealed class BackupWriter : DisposableObjectBase + { + private readonly ZipArchive archive; + private readonly IJsonSerializer serializer; + private readonly Func converter; + private int writtenEvents; + private int writtenAttachments; + + public int WrittenEvents + { + get { return writtenEvents; } + } + + public int WrittenAttachments + { + get { return writtenAttachments; } + } + + public BackupWriter(IJsonSerializer serializer, Stream stream, bool keepOpen = false, BackupVersion version = BackupVersion.V2) + { + Guard.NotNull(serializer); + + this.serializer = serializer; + + converter = + version == BackupVersion.V1 ? + new Func(CompatibleStoredEvent.V1) : + new Func(CompatibleStoredEvent.V2); + + archive = new ZipArchive(stream, ZipArchiveMode.Create, keepOpen); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + archive.Dispose(); + } + } + + public Task WriteJsonAsync(string name, object value) + { + Guard.NotNullOrEmpty(name); + + var attachmentEntry = archive.CreateEntry(ArchiveHelper.GetAttachmentPath(name)); + + using (var stream = attachmentEntry.Open()) + { + serializer.Serialize(value, stream); + } + + writtenAttachments++; + + return TaskHelper.Done; + } + + public async Task WriteBlobAsync(string name, Func handler) + { + Guard.NotNullOrEmpty(name); + Guard.NotNull(handler); + + var attachmentEntry = archive.CreateEntry(ArchiveHelper.GetAttachmentPath(name)); + + using (var stream = attachmentEntry.Open()) + { + await handler(stream); + } + + writtenAttachments++; + } + + public void WriteEvent(StoredEvent storedEvent) + { + Guard.NotNull(storedEvent); + + var eventEntry = archive.CreateEntry(ArchiveHelper.GetEventPath(writtenEvents)); + + using (var stream = eventEntry.Open()) + { + var @event = converter(storedEvent); + + serializer.Serialize(@event, stream); + } + + writtenEvents++; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs new file mode 100644 index 000000000..c1aed7a3f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs @@ -0,0 +1,109 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + internal sealed class GuidMapper + { + private static readonly int GuidLength = Guid.Empty.ToString().Length; + private readonly Dictionary oldToNewGuid = new Dictionary(); + private readonly Dictionary newToOldGuid = new Dictionary(); + private readonly Dictionary strings = new Dictionary(); + + public Guid OldGuid(Guid newGuid) + { + return newToOldGuid.GetOrCreate(newGuid, x => x); + } + + public string? NewGuidOrNull(string value) + { + if (TryGenerateNewGuidString(value, out var result) || TryGenerateNewNamedId(value, out result)) + { + return result; + } + + return null; + } + + public string NewGuidOrValue(string value) + { + if (TryGenerateNewGuidString(value, out var result) || TryGenerateNewNamedId(value, out result)) + { + return result; + } + + return value; + } + + private bool TryGenerateNewGuidString(string value, [MaybeNullWhen(false)] out string result) + { + if (value.Length == GuidLength) + { + if (strings.TryGetValue(value, out result!)) + { + return true; + } + + if (Guid.TryParse(value, out var guid)) + { + var newGuid = GenerateNewGuid(guid); + + strings[value] = result = newGuid.ToString(); + + return true; + } + } + + result = null!; + + return false; + } + + private bool TryGenerateNewNamedId(string value, [MaybeNullWhen(false)] out string result) + { + if (value.Length > GuidLength) + { + if (strings.TryGetValue(value, out result!)) + { + return true; + } + + if (NamedId.TryParse(value, Guid.TryParse, out var namedId)) + { + var newGuid = GenerateNewGuid(namedId.Id); + + strings[value] = result = NamedId.Of(newGuid, namedId.Name).ToString(); + + return true; + } + } + + result = null!; + + return false; + } + + private Guid GenerateNewGuid(Guid oldGuid) + { + return oldToNewGuid.GetOrAdd(oldGuid, GuidGenerator); + } + + private Guid GuidGenerator(Guid oldGuid) + { + var newGuid = Guid.NewGuid(); + + newToOldGuid[newGuid] = oldGuid; + + return newGuid; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs new file mode 100644 index 000000000..1cef6ad2a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs @@ -0,0 +1,87 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Entities.Backup.Helpers +{ + public static class Downloader + { + public static async Task DownloadAsync(this IBackupArchiveLocation backupArchiveLocation, Uri url, string id) + { + if (string.Equals(url.Scheme, "file")) + { + try + { + using (var targetStream = await backupArchiveLocation.OpenStreamAsync(id)) + { + using (var sourceStream = new FileStream(url.LocalPath, FileMode.Open, FileAccess.Read)) + { + await sourceStream.CopyToAsync(targetStream); + } + } + } + catch (IOException ex) + { + throw new BackupRestoreException($"Cannot download the archive: {ex.Message}.", ex); + } + } + else + { + HttpResponseMessage? response = null; + try + { + using (var client = new HttpClient()) + { + response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); + + using (var sourceStream = await response.Content.ReadAsStreamAsync()) + { + using (var targetStream = await backupArchiveLocation.OpenStreamAsync(id)) + { + await sourceStream.CopyToAsync(targetStream); + } + } + } + } + catch (HttpRequestException ex) + { + throw new BackupRestoreException($"Cannot download the archive. Got status code: {response?.StatusCode}.", ex); + } + } + } + + public static async Task OpenArchiveAsync(this IBackupArchiveLocation backupArchiveLocation, string id, IJsonSerializer serializer) + { + Stream? stream = null; + + try + { + stream = await backupArchiveLocation.OpenStreamAsync(id); + + return new BackupReader(serializer, stream); + } + catch (IOException) + { + stream?.Dispose(); + + throw new BackupRestoreException("The backup archive is correupt and cannot be opened."); + } + catch (Exception) + { + stream?.Dispose(); + + throw; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Safe.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Safe.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/Helpers/Safe.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Safe.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs new file mode 100644 index 000000000..ada75140f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public interface IRestoreGrain : IGrainWithStringKey + { + Task RestoreAsync(Uri url, RefToken actor, string? newAppName = null); + + Task> GetJobAsync(); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/IRestoreJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreJob.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/IRestoreJob.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreJob.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/JobStatus.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/JobStatus.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/JobStatus.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/JobStatus.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs new file mode 100644 index 000000000..38ebeb0a2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs @@ -0,0 +1,367 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Backup.Helpers; +using Squidex.Domain.Apps.Entities.Backup.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public sealed class RestoreGrain : GrainOfString, IRestoreGrain + { + private readonly IBackupArchiveLocation backupArchiveLocation; + private readonly IClock clock; + private readonly ICommandBus commandBus; + private readonly IJsonSerializer serializer; + private readonly IEventStore eventStore; + private readonly IEventDataFormatter eventDataFormatter; + private readonly ISemanticLog log; + private readonly IServiceProvider serviceProvider; + private readonly IStreamNameResolver streamNameResolver; + private readonly IGrainState state; + + private RestoreStateJob CurrentJob + { + get { return state.Value.Job; } + } + + public RestoreGrain(IBackupArchiveLocation backupArchiveLocation, + IClock clock, + ICommandBus commandBus, + IEventStore eventStore, + IEventDataFormatter eventDataFormatter, + IJsonSerializer serializer, + ISemanticLog log, + IServiceProvider serviceProvider, + IStreamNameResolver streamNameResolver, + IGrainState state) + { + Guard.NotNull(backupArchiveLocation); + Guard.NotNull(clock); + Guard.NotNull(commandBus); + Guard.NotNull(eventStore); + Guard.NotNull(eventDataFormatter); + Guard.NotNull(serializer); + Guard.NotNull(serviceProvider); + Guard.NotNull(state); + Guard.NotNull(streamNameResolver); + Guard.NotNull(log); + + this.backupArchiveLocation = backupArchiveLocation; + this.clock = clock; + this.commandBus = commandBus; + this.eventStore = eventStore; + this.eventDataFormatter = eventDataFormatter; + this.serializer = serializer; + this.serviceProvider = serviceProvider; + this.streamNameResolver = streamNameResolver; + this.state = state; + this.log = log; + } + + protected override Task OnActivateAsync(string key) + { + RecoverAfterRestartAsync().Forget(); + + return TaskHelper.Done; + } + + private async Task RecoverAfterRestartAsync() + { + if (CurrentJob?.Status == JobStatus.Started) + { + var handlers = CreateHandlers(); + + Log("Failed due application restart"); + + CurrentJob.Status = JobStatus.Failed; + + await CleanupAsync(handlers); + + await state.WriteAsync(); + } + } + + public async Task RestoreAsync(Uri url, RefToken actor, string? newAppName) + { + Guard.NotNull(url); + Guard.NotNull(actor); + + if (!string.IsNullOrWhiteSpace(newAppName)) + { + Guard.ValidSlug(newAppName); + } + + if (CurrentJob?.Status == JobStatus.Started) + { + throw new DomainException("A restore operation is already running."); + } + + state.Value.Job = new RestoreStateJob + { + Id = Guid.NewGuid(), + NewAppName = newAppName, + Actor = actor, + Started = clock.GetCurrentInstant(), + Status = JobStatus.Started, + Url = url + }; + + await state.WriteAsync(); + + Process(); + } + + private void Process() + { + ProcessAsync().Forget(); + } + + private async Task ProcessAsync() + { + var handlers = CreateHandlers(); + + var logContext = (jobId: CurrentJob.Id.ToString(), jobUrl: CurrentJob.Url.ToString()); + + using (Profiler.StartSession()) + { + try + { + Log("Started. The restore process has the following steps:"); + Log(" * Download backup"); + Log(" * Restore events and attachments."); + Log(" * Restore all objects like app, schemas and contents"); + Log(" * Complete the restore operation for all objects"); + + log.LogInformation(logContext, (ctx, w) => w + .WriteProperty("action", "restore") + .WriteProperty("status", "started") + .WriteProperty("operationId", ctx.jobId) + .WriteProperty("url", ctx.jobUrl)); + + using (Profiler.Trace("Download")) + { + await DownloadAsync(); + } + + using (var reader = await backupArchiveLocation.OpenArchiveAsync(CurrentJob.Id.ToString(), serializer)) + { + using (Profiler.Trace("ReadEvents")) + { + await ReadEventsAsync(reader, handlers); + } + + foreach (var handler in handlers) + { + using (Profiler.TraceMethod(handler.GetType(), nameof(BackupHandler.RestoreAsync))) + { + await handler.RestoreAsync(CurrentJob.AppId, reader); + } + + Log($"Restored {handler.Name}"); + } + + foreach (var handler in handlers) + { + using (Profiler.TraceMethod(handler.GetType(), nameof(BackupHandler.CompleteRestoreAsync))) + { + await handler.CompleteRestoreAsync(CurrentJob.AppId, reader); + } + + Log($"Completed {handler.Name}"); + } + } + + await AssignContributorAsync(); + + CurrentJob.Status = JobStatus.Completed; + + Log("Completed, Yeah!"); + + log.LogInformation(logContext, (ctx, w) => + { + w.WriteProperty("action", "restore"); + w.WriteProperty("status", "completed"); + w.WriteProperty("operationId", ctx.jobId); + w.WriteProperty("url", ctx.jobUrl); + + Profiler.Session?.Write(w); + }); + } + catch (Exception ex) + { + if (ex is BackupRestoreException backupException) + { + Log(backupException.Message); + } + else + { + Log("Failed with internal error"); + } + + await CleanupAsync(handlers); + + CurrentJob.Status = JobStatus.Failed; + + log.LogError(ex, logContext, (ctx, w) => + { + w.WriteProperty("action", "retore"); + w.WriteProperty("status", "failed"); + w.WriteProperty("operationId", ctx.jobId); + w.WriteProperty("url", ctx.jobUrl); + + Profiler.Session?.Write(w); + }); + } + finally + { + CurrentJob.Stopped = clock.GetCurrentInstant(); + + await state.WriteAsync(); + } + } + } + + private async Task AssignContributorAsync() + { + var actor = CurrentJob.Actor; + + if (actor?.IsSubject == true) + { + try + { + await commandBus.PublishAsync(new AssignContributor + { + Actor = actor, + AppId = CurrentJob.AppId, + ContributorId = actor.Identifier, + IsRestore = true, + Role = Role.Owner + }); + + Log("Assigned current user."); + } + catch (DomainException ex) + { + Log($"Failed to assign contributor: {ex.Message}"); + } + } + else + { + Log("Current user not assigned because restore was triggered by client."); + } + } + + private async Task CleanupAsync(IEnumerable handlers) + { + await Safe.DeleteAsync(backupArchiveLocation, CurrentJob.Id.ToString(), log); + + if (CurrentJob.AppId != Guid.Empty) + { + foreach (var handler in handlers) + { + await Safe.CleanupRestoreErrorAsync(handler, CurrentJob.AppId, CurrentJob.Id, log); + } + } + } + + private async Task DownloadAsync() + { + Log("Downloading Backup"); + + await backupArchiveLocation.DownloadAsync(CurrentJob.Url, CurrentJob.Id.ToString()); + + Log("Downloaded Backup"); + } + + private async Task ReadEventsAsync(BackupReader reader, IEnumerable handlers) + { + await reader.ReadEventsAsync(streamNameResolver, eventDataFormatter, async storedEvent => + { + await HandleEventAsync(reader, handlers, storedEvent.Stream, storedEvent.Event); + }); + + Log($"Reading {reader.ReadEvents} events and {reader.ReadAttachments} attachments completed.", true); + } + + private async Task HandleEventAsync(BackupReader reader, IEnumerable handlers, string stream, Envelope @event) + { + if (@event.Payload is SquidexEvent squidexEvent) + { + squidexEvent.Actor = CurrentJob.Actor; + } + + if (@event.Payload is AppCreated appCreated) + { + CurrentJob.AppId = appCreated.AppId.Id; + + if (!string.IsNullOrWhiteSpace(CurrentJob.NewAppName)) + { + appCreated.Name = CurrentJob.NewAppName; + } + } + + if (@event.Payload is AppEvent appEvent && !string.IsNullOrWhiteSpace(CurrentJob.NewAppName)) + { + appEvent.AppId = NamedId.Of(appEvent.AppId.Id, CurrentJob.NewAppName); + } + + foreach (var handler in handlers) + { + if (!await handler.RestoreEventAsync(@event, CurrentJob.AppId, reader, CurrentJob.Actor)) + { + return; + } + } + + var eventData = eventDataFormatter.ToEventData(@event, @event.Headers.CommitId()); + var eventCommit = new List { eventData }; + + await eventStore.AppendAsync(Guid.NewGuid(), stream, eventCommit); + + Log($"Read {reader.ReadEvents} events and {reader.ReadAttachments} attachments.", true); + } + + private void Log(string message, bool replace = false) + { + if (replace && CurrentJob.Log.Count > 0) + { + CurrentJob.Log[CurrentJob.Log.Count - 1] = $"{clock.GetCurrentInstant()}: {message}"; + } + else + { + CurrentJob.Log.Add($"{clock.GetCurrentInstant()}: {message}"); + } + } + + private IEnumerable CreateHandlers() + { + return serviceProvider.GetRequiredService>(); + } + + public Task> GetJobAsync() + { + return Task.FromResult>(CurrentJob); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreState.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreState.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/State/RestoreState.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreState.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs new file mode 100644 index 000000000..5d9e58f8c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Backup.State +{ + [DataContract] + public sealed class RestoreStateJob : IRestoreJob + { + [DataMember] + public string AppName { get; set; } + + [DataMember] + public Guid Id { get; set; } + + [DataMember] + public Guid AppId { get; set; } + + [DataMember] + public RefToken Actor { get; set; } + + [DataMember] + public Uri Url { get; set; } + + [DataMember] + public string? NewAppName { get; set; } + + [DataMember] + public Instant Started { get; set; } + + [DataMember] + public Instant? Stopped { get; set; } + + [DataMember] + public List Log { get; set; } = new List(); + + [DataMember] + public JobStatus Status { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs diff --git a/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs diff --git a/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs diff --git a/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs new file mode 100644 index 000000000..2fe683b8f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs @@ -0,0 +1,126 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Comments.Commands; +using Squidex.Domain.Apps.Entities.Comments.Guards; +using Squidex.Domain.Apps.Entities.Comments.State; +using Squidex.Domain.Apps.Events.Comments; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Comments +{ + public sealed class CommentsGrain : DomainObjectGrainBase, ICommentsGrain + { + private readonly IStore store; + private readonly List> events = new List>(); + private CommentsState snapshot = new CommentsState { Version = EtagVersion.Empty }; + private IPersistence persistence; + + public override CommentsState Snapshot + { + get { return snapshot; } + } + + public CommentsGrain(IStore store, ISemanticLog log) + : base(log) + { + Guard.NotNull(store); + + this.store = store; + } + + protected override void ApplyEvent(Envelope @event) + { + snapshot = new CommentsState { Version = snapshot.Version + 1 }; + + events.Add(@event.To()); + } + + protected override void RestorePreviousSnapshot(CommentsState previousSnapshot, long previousVersion) + { + snapshot = previousSnapshot; + } + + protected override Task ReadAsync(Type type, Guid id) + { + persistence = store.WithEventSourcing(GetType(), id, ApplyEvent); + + return persistence.ReadAsync(); + } + + protected override async Task WriteAsync(Envelope[] events, long previousVersion) + { + if (events.Length > 0) + { + await persistence.WriteEventsAsync(events); + } + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + switch (command) + { + case CreateComment createComment: + return UpsertReturn(createComment, c => + { + GuardComments.CanCreate(c); + + Create(c); + + return EntityCreatedResult.Create(createComment.CommentId, Version); + }); + + case UpdateComment updateComment: + return Upsert(updateComment, c => + { + GuardComments.CanUpdate(events, c); + + Update(c); + }); + + case DeleteComment deleteComment: + return Upsert(deleteComment, c => + { + GuardComments.CanDelete(events, c); + + Delete(c); + }); + + default: + throw new NotSupportedException(); + } + } + + public void Create(CreateComment command) + { + RaiseEvent(SimpleMapper.Map(command, new CommentCreated())); + } + + public void Update(UpdateComment command) + { + RaiseEvent(SimpleMapper.Map(command, new CommentUpdated())); + } + + public void Delete(DeleteComment command) + { + RaiseEvent(SimpleMapper.Map(command, new CommentDeleted())); + } + + public Task GetCommentsAsync(long version = EtagVersion.Any) + { + return Task.FromResult(CommentsResult.FromEvents(events, Version, (int)version)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs diff --git a/src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs new file mode 100644 index 000000000..b3c039965 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs @@ -0,0 +1,89 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Entities.Comments.Commands; +using Squidex.Domain.Apps.Events.Comments; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Comments.Guards +{ + public static class GuardComments + { + public static void CanCreate(CreateComment command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot create comment.", e => + { + if (string.IsNullOrWhiteSpace(command.Text)) + { + e(Not.Defined("Text"), nameof(command.Text)); + } + }); + } + + public static void CanUpdate(List> events, UpdateComment command) + { + Guard.NotNull(command); + + var comment = FindComment(events, command.CommentId); + + if (!comment.Payload.Actor.Equals(command.Actor)) + { + throw new DomainException("Comment is created by another actor."); + } + + Validate.It(() => "Cannot update comment.", e => + { + if (string.IsNullOrWhiteSpace(command.Text)) + { + e(Not.Defined("Text"), nameof(command.Text)); + } + }); + } + + public static void CanDelete(List> events, DeleteComment command) + { + Guard.NotNull(command); + + var comment = FindComment(events, command.CommentId); + + if (!comment.Payload.Actor.Equals(command.Actor)) + { + throw new DomainException("Comment is created by another actor."); + } + } + + private static Envelope FindComment(List> events, Guid commentId) + { + Envelope? result = null; + + foreach (var @event in events) + { + if (@event.Payload is CommentCreated created && created.CommentId == commentId) + { + result = @event.To(); + } + else if (@event.Payload is CommentDeleted deleted && deleted.CommentId == commentId) + { + result = null; + } + } + + if (result == null) + { + throw new DomainObjectNotFoundException(commentId.ToString(), "Comments", typeof(CommentsGrain)); + } + + return result; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Comments/ICommentsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/ICommentsGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs diff --git a/src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/DiscardChanges.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DiscardChanges.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/DiscardChanges.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DiscardChanges.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs new file mode 100644 index 000000000..6f436a0be --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -0,0 +1,133 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// 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.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentChangedTriggerHandler : RuleTriggerHandler + { + private readonly IScriptEngine scriptEngine; + private readonly IContentLoader contentLoader; + + public ContentChangedTriggerHandler(IScriptEngine scriptEngine, IContentLoader contentLoader) + { + Guard.NotNull(scriptEngine); + Guard.NotNull(contentLoader); + + this.scriptEngine = scriptEngine; + + this.contentLoader = contentLoader; + } + + protected override async Task CreateEnrichedEventAsync(Envelope @event) + { + var result = new EnrichedContentEvent(); + + var content = await contentLoader.GetAsync(@event.Headers.AggregateId(), @event.Headers.EventStreamNumber()); + + SimpleMapper.Map(content, result); + + result.Data = content.Data ?? content.DataDraft; + + switch (@event.Payload) + { + case ContentCreated _: + result.Type = EnrichedContentEventType.Created; + break; + case ContentDeleted _: + result.Type = EnrichedContentEventType.Deleted; + break; + case ContentChangesPublished _: + case ContentUpdated _: + result.Type = EnrichedContentEventType.Updated; + break; + case ContentStatusChanged contentStatusChanged: + switch (contentStatusChanged.Change) + { + case StatusChange.Published: + result.Type = EnrichedContentEventType.Published; + break; + case StatusChange.Unpublished: + result.Type = EnrichedContentEventType.Unpublished; + break; + default: + result.Type = EnrichedContentEventType.StatusChanged; + break; + } + + break; + } + + result.Name = $"{content.SchemaId.Name.ToPascalCase()}{result.Type}"; + + return result; + } + + protected override bool Trigger(ContentEvent @event, ContentChangedTriggerV2 trigger, Guid ruleId) + { + if (trigger.HandleAll) + { + return true; + } + + if (trigger.Schemas != null) + { + foreach (var schema in trigger.Schemas) + { + if (MatchsSchema(schema, @event.SchemaId)) + { + return true; + } + } + } + + return false; + } + + protected override bool Trigger(EnrichedContentEvent @event, ContentChangedTriggerV2 trigger) + { + if (trigger.HandleAll) + { + return true; + } + + if (trigger.Schemas != null) + { + foreach (var schema in trigger.Schemas) + { + if (MatchsSchema(schema, @event.SchemaId) && MatchsCondition(schema, @event)) + { + return true; + } + } + } + + return false; + } + + private static bool MatchsSchema(ContentChangedTriggerSchemaV2 schema, NamedId eventId) + { + return eventId.Id == schema.SchemaId; + } + + private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEventBase @event) + { + return string.IsNullOrWhiteSpace(schema.Condition) || scriptEngine.Evaluate("event", @event, schema.Condition); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs new file mode 100644 index 000000000..6b002ad96 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentCommandMiddleware : GrainCommandMiddleware + { + private readonly IContentEnricher contentEnricher; + private readonly IContextProvider contextProvider; + + public ContentCommandMiddleware(IGrainFactory grainFactory, IContentEnricher contentEnricher, IContextProvider contextProvider) + : base(grainFactory) + { + Guard.NotNull(contentEnricher); + Guard.NotNull(contextProvider); + + this.contentEnricher = contentEnricher; + this.contextProvider = contextProvider; + } + + public override async Task HandleAsync(CommandContext context, Func next) + { + await base.HandleAsync(context, next); + + if (context.PlainResult is IContentEntity content && NotEnriched(context)) + { + var enriched = await contentEnricher.EnrichAsync(content, contextProvider.Context); + + context.Complete(enriched); + } + } + + private static bool NotEnriched(CommandContext context) + { + return !(context.PlainResult is IEnrichedContentEntity); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs new file mode 100644 index 000000000..ebfd08243 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentEntity : IEnrichedContentEntity + { + public Guid Id { get; set; } + + public NamedId AppId { get; set; } + + public NamedId SchemaId { get; set; } + + public long Version { get; set; } + + public Instant Created { get; set; } + + public Instant LastModified { get; set; } + + public RefToken CreatedBy { get; set; } + + public RefToken LastModifiedBy { get; set; } + + public ScheduleJob ScheduleJob { get; set; } + + public NamedContentData? Data { get; set; } + + public NamedContentData DataDraft { get; set; } + + public NamedContentData? ReferenceData { get; set; } + + public Status Status { get; set; } + + public StatusInfo[]? Nexts { get; set; } + + public string StatusColor { get; set; } + + public string SchemaName { get; set; } + + public string SchemaDisplayName { get; set; } + + public RootField[]? ReferenceFields { get; set; } + + public bool CanUpdate { get; set; } + + public bool IsPending { get; set; } + + public HashSet CacheDependencies { get; set; } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs new file mode 100644 index 000000000..af04ee31b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -0,0 +1,377 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// 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.Scripting; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Guards; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentGrain : LogSnapshotDomainObjectGrain, IContentGrain + { + private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); + private readonly IAppProvider appProvider; + private readonly IAssetRepository assetRepository; + private readonly IContentRepository contentRepository; + private readonly IScriptEngine scriptEngine; + private readonly IContentWorkflow contentWorkflow; + + public ContentGrain( + IStore store, + ISemanticLog log, + IAppProvider appProvider, + IAssetRepository assetRepository, + IScriptEngine scriptEngine, + IContentWorkflow contentWorkflow, + IContentRepository contentRepository, + IActivationLimit limit) + : base(store, log) + { + Guard.NotNull(appProvider); + Guard.NotNull(scriptEngine); + Guard.NotNull(assetRepository); + Guard.NotNull(contentWorkflow); + Guard.NotNull(contentRepository); + + this.appProvider = appProvider; + this.scriptEngine = scriptEngine; + this.assetRepository = assetRepository; + this.contentWorkflow = contentWorkflow; + this.contentRepository = contentRepository; + + limit?.SetLimit(5000, Lifetime); + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + VerifyNotDeleted(); + + switch (command) + { + case CreateContent createContent: + return CreateReturnAsync(createContent, async c => + { + var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, c, () => "Failed to create content."); + + var status = (await contentWorkflow.GetInitialStatusAsync(ctx.Schema)).Status; + + await GuardContent.CanCreate(ctx.Schema, contentWorkflow, c); + + c.Data = await ctx.ExecuteScriptAndTransformAsync(s => s.Create, + new ScriptContext + { + Operation = "Create", + Data = c.Data, + Status = status, + StatusOld = default + }); + + await ctx.EnrichAsync(c.Data); + + if (!c.DoNotValidate) + { + await ctx.ValidateAsync(c.Data); + } + + if (c.Publish) + { + await ctx.ExecuteScriptAsync(s => s.Change, + new ScriptContext + { + Operation = "Published", + Data = c.Data, + Status = Status.Published, + StatusOld = default + }); + } + + Create(c, status); + + return Snapshot; + }); + + case UpdateContent updateContent: + return UpdateReturnAsync(updateContent, async c => + { + var isProposal = c.AsDraft && Snapshot.Status == Status.Published; + + await GuardContent.CanUpdate(Snapshot, contentWorkflow, c, isProposal); + + return await UpdateAsync(c, x => c.Data, false, isProposal); + }); + + case PatchContent patchContent: + return UpdateReturnAsync(patchContent, async c => + { + var isProposal = IsProposal(c); + + await GuardContent.CanPatch(Snapshot, contentWorkflow, c, isProposal); + + return await UpdateAsync(c, c.Data.MergeInto, true, isProposal); + }); + + case ChangeContentStatus changeContentStatus: + return UpdateReturnAsync(changeContentStatus, async c => + { + try + { + var isChangeConfirm = IsConfirm(c); + + var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to change content."); + + await GuardContent.CanChangeStatus(ctx.Schema, Snapshot, contentWorkflow, c, isChangeConfirm); + + if (c.DueTime.HasValue) + { + ScheduleStatus(c); + } + else + { + if (isChangeConfirm) + { + ConfirmChanges(c); + } + else + { + var change = GetChange(c); + + await ctx.ExecuteScriptAsync(s => s.Change, + new ScriptContext + { + Operation = change.ToString(), + Data = Snapshot.Data, + Status = c.Status, + StatusOld = Snapshot.Status + }); + + ChangeStatus(c, change); + } + } + } + catch (Exception) + { + if (c.JobId.HasValue && Snapshot?.ScheduleJob?.Id == c.JobId) + { + CancelScheduling(c); + } + else + { + throw; + } + } + + return Snapshot; + }); + + case DiscardChanges discardChanges: + return UpdateReturn(discardChanges, c => + { + GuardContent.CanDiscardChanges(Snapshot.IsPending, c); + + DiscardChanges(c); + + return Snapshot; + }); + + case DeleteContent deleteContent: + return UpdateAsync(deleteContent, async c => + { + var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to delete content."); + + GuardContent.CanDelete(ctx.Schema, c); + + await ctx.ExecuteScriptAsync(s => s.Delete, + new ScriptContext + { + Operation = "Delete", + Data = Snapshot.Data, + Status = Snapshot.Status, + StatusOld = default + }); + + Delete(c); + }); + + default: + throw new NotSupportedException(); + } + } + + private async Task UpdateAsync(ContentUpdateCommand command, Func newDataFunc, bool partial, bool isProposal) + { + var currentData = + isProposal ? + Snapshot.DataDraft : + Snapshot.Data; + + var newData = newDataFunc(currentData!); + + if (!currentData!.Equals(newData)) + { + var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, command, () => "Failed to update content."); + + if (partial) + { + await ctx.ValidatePartialAsync(command.Data); + } + else + { + await ctx.ValidateAsync(command.Data); + } + + newData = await ctx.ExecuteScriptAndTransformAsync(s => s.Update, + new ScriptContext + { + Operation = "Create", + Data = newData, + DataOld = currentData, + Status = Snapshot.Status, + StatusOld = default + }); + + if (isProposal) + { + ProposeUpdate(command, newData); + } + else + { + Update(command, newData); + } + } + + return Snapshot; + } + + public void Create(CreateContent command, Status status) + { + RaiseEvent(SimpleMapper.Map(command, new ContentCreated { Status = status })); + + if (command.Publish) + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published })); + } + } + + public void ConfirmChanges(ChangeContentStatus command) + { + RaiseEvent(SimpleMapper.Map(command, new ContentChangesPublished())); + } + + public void DiscardChanges(DiscardChanges command) + { + RaiseEvent(SimpleMapper.Map(command, new ContentChangesDiscarded())); + } + + public void Delete(DeleteContent command) + { + RaiseEvent(SimpleMapper.Map(command, new ContentDeleted())); + } + + public void Update(ContentCommand command, NamedContentData data) + { + RaiseEvent(SimpleMapper.Map(command, new ContentUpdated { Data = data })); + } + + public void ProposeUpdate(ContentCommand command, NamedContentData data) + { + RaiseEvent(SimpleMapper.Map(command, new ContentUpdateProposed { Data = data })); + } + + public void CancelScheduling(ChangeContentStatus command) + { + RaiseEvent(SimpleMapper.Map(command, new ContentSchedulingCancelled())); + } + + public void ScheduleStatus(ChangeContentStatus command) + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime!.Value })); + } + + public void ChangeStatus(ChangeContentStatus command, StatusChange change) + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = change })); + } + + private void RaiseEvent(SchemaEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + if (@event.SchemaId == null) + { + @event.SchemaId = Snapshot.SchemaId; + } + + RaiseEvent(Envelope.Create(@event)); + } + + private bool IsConfirm(ChangeContentStatus command) + { + return Snapshot.IsPending && Snapshot.Status == Status.Published && command.Status == Status.Published; + } + + private bool IsProposal(PatchContent command) + { + return Snapshot.Status == Status.Published && command.AsDraft; + } + + private StatusChange GetChange(ChangeContentStatus command) + { + var change = StatusChange.Change; + + if (command.Status == Status.Published) + { + change = StatusChange.Published; + } + else if (Snapshot.Status == Status.Published) + { + change = StatusChange.Unpublished; + } + + return change; + } + + private void VerifyNotDeleted() + { + if (Snapshot.IsDeleted) + { + throw new DomainException("Content has already been deleted."); + } + } + + private async Task CreateContext(Guid appId, Guid schemaId, ContentCommand command, Func message) + { + var operationContext = + await ContentOperationContext.CreateAsync(appId, schemaId, command, + appProvider, assetRepository, contentRepository, scriptEngine, message); + + return operationContext; + } + + public Task> GetStateAsync(long version = EtagVersion.Any) + { + return J.AsTask(GetSnapshot(version)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs new file mode 100644 index 000000000..b572f431b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase + { + public ContentHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) + { + AddEventMessage( + "created {[Schema]} content."); + + AddEventMessage( + "updated {[Schema]} content."); + + AddEventMessage( + "deleted {[Schema]} content."); + + AddEventMessage( + "discarded pending changes of {[Schema]} content."); + + AddEventMessage( + "published changes of {[Schema]} content."); + + AddEventMessage( + "proposed update for {[Schema]} content."); + + AddEventMessage( + "failed to schedule status change for {[Schema]} content."); + + AddEventMessage( + "changed status of {[Schema]} content to {[Status]}."); + + AddEventMessage( + "scheduled to change status of {[Schema]} content to {[Status]}."); + } + + protected override Task CreateEventCoreAsync(Envelope @event) + { + var channel = $"contents.{@event.Headers.AggregateId()}"; + + HistoryEvent? result = ForEvent(@event.Payload, channel); + + if (@event.Payload is SchemaEvent schemaEvent) + { + result = result.Param("Schema", schemaEvent.SchemaId.Name); + } + + if (@event.Payload is ContentStatusChanged contentStatusChanged) + { + result = result.Param("Status", contentStatusChanged.Status); + } + + if (@event.Payload is ContentStatusScheduled contentStatusScheduled) + { + result = result.Param("Status", contentStatusScheduled.Status); + } + + return Task.FromResult(result); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs new file mode 100644 index 000000000..c1c27a52e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs @@ -0,0 +1,153 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.EnrichContent; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentOperationContext + { + private IContentRepository contentRepository; + private IAssetRepository assetRepository; + private IScriptEngine scriptEngine; + private ISchemaEntity schemaEntity; + private IAppEntity appEntity; + private ContentCommand command; + private Guid schemaId; + private Func message; + + public ISchemaEntity Schema + { + get { return schemaEntity; } + } + + public static async Task CreateAsync( + Guid appId, + Guid schemaId, + ContentCommand command, + IAppProvider appProvider, + IAssetRepository assetRepository, + IContentRepository contentRepository, + IScriptEngine scriptEngine, + Func message) + { + var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(appId, schemaId); + + if (appEntity == null) + { + throw new InvalidOperationException("Cannot resolve app."); + } + + if (schemaEntity == null) + { + throw new InvalidOperationException("Cannot resolve schema."); + } + + var context = new ContentOperationContext + { + appEntity = appEntity, + assetRepository = assetRepository, + command = command, + contentRepository = contentRepository, + message = message, + schemaId = schemaId, + schemaEntity = schemaEntity, + scriptEngine = scriptEngine + }; + + return context; + } + + public Task EnrichAsync(NamedContentData data) + { + data.Enrich(schemaEntity.SchemaDef, appEntity.PartitionResolver()); + + return TaskHelper.Done; + } + + public Task ValidateAsync(NamedContentData data) + { + var ctx = CreateValidationContext(); + + return data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); + } + + public Task ValidatePartialAsync(NamedContentData data) + { + var ctx = CreateValidationContext(); + + return data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); + } + + public Task ExecuteScriptAndTransformAsync(Func script, ScriptContext context) + { + Enrich(context); + + var result = scriptEngine.ExecuteAndTransform(context, GetScript(script)); + + return Task.FromResult(result); + } + + public Task ExecuteScriptAsync(Func script, ScriptContext context) + { + Enrich(context); + + scriptEngine.Execute(context, GetScript(script)); + + return TaskHelper.Done; + } + + private void Enrich(ScriptContext context) + { + context.ContentId = command.ContentId; + + context.User = command.User; + } + + private ValidationContext CreateValidationContext() + { + return new ValidationContext(command.ContentId, schemaId, + QueryContentsAsync, + QueryContentsAsync, + QueryAssetsAsync); + } + + private async Task> QueryAssetsAsync(IEnumerable assetIds) + { + return await assetRepository.QueryAsync(appEntity.Id, new HashSet(assetIds)); + } + + private async Task> QueryContentsAsync(Guid filterSchemaId, FilterNode filterNode) + { + return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode); + } + + private async Task> QueryContentsAsync(HashSet ids) + { + return await contentRepository.QueryIdsAsync(appEntity.Id, ids); + } + + private string GetScript(Func script) + { + return script(schemaEntity.SchemaDef.Scripts); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs new file mode 100644 index 000000000..c63c25a81 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs @@ -0,0 +1,107 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using NodaTime; +using Orleans; +using Orleans.Runtime; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentSchedulerGrain : Grain, IContentSchedulerGrain, IRemindable + { + private readonly IContentRepository contentRepository; + private readonly ICommandBus commandBus; + private readonly IClock clock; + private readonly ISemanticLog log; + private TaskScheduler scheduler; + + public ContentSchedulerGrain( + IContentRepository contentRepository, + ICommandBus commandBus, + IClock clock, + ISemanticLog log) + { + Guard.NotNull(contentRepository); + Guard.NotNull(commandBus); + Guard.NotNull(clock); + Guard.NotNull(log); + + this.clock = clock; + + this.commandBus = commandBus; + this.contentRepository = contentRepository; + + this.log = log; + } + + public override Task OnActivateAsync() + { + scheduler = TaskScheduler.Current; + + DelayDeactivation(TimeSpan.FromDays(1)); + + RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + RegisterTimer(x => PublishAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); + + return Task.FromResult(true); + } + + public Task ActivateAsync() + { + return TaskHelper.Done; + } + + public Task PublishAsync() + { + var now = clock.GetCurrentInstant(); + + return contentRepository.QueryScheduledWithoutDataAsync(now, content => + { + return Dispatch(async () => + { + try + { + var job = content.ScheduleJob; + + if (job != null) + { + var command = new ChangeContentStatus { ContentId = content.Id, Status = job.Status, Actor = job.ScheduledBy, JobId = job.Id }; + + await commandBus.PublishAsync(command); + } + } + catch (Exception ex) + { + log.LogError(ex, content.Id.ToString(), (logContentId, w) => w + .WriteProperty("action", "ChangeStatusScheduled") + .WriteProperty("status", "Failed") + .WriteProperty("contentId", logContentId)); + } + }); + }); + } + + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return TaskHelper.Done; + } + + private Task Dispatch(Func task) + { + return Task.Factory.StartNew(task, CancellationToken.None, TaskCreationOptions.None, scheduler ?? TaskScheduler.Default).Unwrap(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs new file mode 100644 index 000000000..bc3d4344a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class DefaultWorkflowsValidator : IWorkflowsValidator + { + private readonly IAppProvider appProvider; + + public DefaultWorkflowsValidator(IAppProvider appProvider) + { + Guard.NotNull(appProvider); + + this.appProvider = appProvider; + } + + public async Task> ValidateAsync(Guid appId, Workflows workflows) + { + Guard.NotNull(workflows); + + var errors = new List(); + + if (workflows.Values.Count(x => x.SchemaIds.Count == 0) > 1) + { + errors.Add("Multiple workflows cover all schemas."); + } + + var uniqueSchemaIds = workflows.Values.SelectMany(x => x.SchemaIds).Distinct().ToList(); + + foreach (var schemaId in uniqueSchemaIds) + { + if (workflows.Values.Count(x => x.SchemaIds.Contains(schemaId)) > 1) + { + var schema = await appProvider.GetSchemaAsync(appId, schemaId); + + if (schema != null) + { + errors.Add($"The schema `{schema.SchemaDef.Name}` is covered by multiple workflows."); + } + } + } + + return errors; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs new file mode 100644 index 000000000..72b45c8ca --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs @@ -0,0 +1,153 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class DynamicContentWorkflow : IContentWorkflow + { + private readonly IScriptEngine scriptEngine; + private readonly IAppProvider appProvider; + + public DynamicContentWorkflow(IScriptEngine scriptEngine, IAppProvider appProvider) + { + Guard.NotNull(scriptEngine); + Guard.NotNull(appProvider); + + this.scriptEngine = scriptEngine; + + this.appProvider = appProvider; + } + + public async Task GetAllAsync(ISchemaEntity schema) + { + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); + + return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray(); + } + + public async Task CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user) + { + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); + + return workflow.TryGetTransition(content.Status, next, out var transition) && CanUse(transition, content.DataDraft, user); + } + + public async Task CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user) + { + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); + + return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && CanUse(transition, data, user); + } + + public async Task CanUpdateAsync(IContentEntity content) + { + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); + + if (workflow.TryGetStep(content.Status, out var step)) + { + return !step.NoUpdate; + } + + return true; + } + + public async Task GetInfoAsync(IContentEntity content) + { + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); + + if (workflow.TryGetStep(content.Status, out var step)) + { + return new StatusInfo(content.Status, GetColor(step)); + } + + return new StatusInfo(content.Status, StatusColors.Draft); + } + + public async Task GetInitialStatusAsync(ISchemaEntity schema) + { + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); + + var (status, step) = workflow.GetInitialStep(); + + return new StatusInfo(status, GetColor(step)); + } + + public async Task GetNextsAsync(IContentEntity content, ClaimsPrincipal user) + { + var result = new List(); + + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); + + foreach (var (to, step, transition) in workflow.GetTransitions(content.Status)) + { + if (CanUse(transition, content.DataDraft, user)) + { + result.Add(new StatusInfo(to, GetColor(step))); + } + } + + return result.ToArray(); + } + + private bool CanUse(WorkflowTransition transition, NamedContentData data, ClaimsPrincipal user) + { + if (transition.Roles != null) + { + if (!user.Claims.Any(x => x.Type == ClaimTypes.Role && transition.Roles.Contains(x.Value))) + { + return false; + } + } + + if (!string.IsNullOrWhiteSpace(transition.Expression)) + { + return scriptEngine.Evaluate("data", data, transition.Expression); + } + + return true; + } + + private async Task GetWorkflowAsync(Guid appId, Guid schemaId) + { + Workflow? result = null; + + var app = await appProvider.GetAppAsync(appId); + + if (app != null) + { + result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Contains(schemaId)); + + if (result == null) + { + result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Count == 0); + } + } + + if (result == null) + { + result = Workflow.Default; + } + + return result; + } + + private static string GetColor(WorkflowStep step) + { + return step.Color ?? StatusColors.Draft; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs new file mode 100644 index 000000000..a58359245 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -0,0 +1,114 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using GraphQL; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService + { + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); + private readonly IDependencyResolver resolver; + + public CachingGraphQLService(IMemoryCache cache, IDependencyResolver resolver) + : base(cache) + { + Guard.NotNull(resolver); + + this.resolver = resolver; + } + + public async Task<(bool HasError, object Response)> QueryAsync(Context context, params GraphQLQuery[] queries) + { + Guard.NotNull(context); + Guard.NotNull(queries); + + var model = await GetModelAsync(context.App); + + var ctx = new GraphQLExecutionContext(context, resolver); + + var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, ctx, q))); + + return (result.Any(x => x.HasError), result.Map(x => x.Response)); + } + + public async Task<(bool HasError, object Response)> QueryAsync(Context context, GraphQLQuery query) + { + Guard.NotNull(context); + Guard.NotNull(query); + + var model = await GetModelAsync(context.App); + + var ctx = new GraphQLExecutionContext(context, resolver); + + var result = await QueryInternalAsync(model, ctx, query); + + return result; + } + + private static async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext ctx, GraphQLQuery query) + { + if (string.IsNullOrWhiteSpace(query.Query)) + { + return (false, new { data = new object() }); + } + + var (data, errors) = await model.ExecuteAsync(ctx, query); + + if (errors?.Any() == true) + { + return (false, new { data, errors }); + } + else + { + return (false, new { data }); + } + } + + private Task GetModelAsync(IAppEntity app) + { + var cacheKey = CreateCacheKey(app.Id, app.Version.ToString()); + + return Cache.GetOrCreateAsync(cacheKey, async entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheDuration; + + var allSchemas = await resolver.Resolve().GetSchemasAsync(app.Id); + + return new GraphQLModel(app, + allSchemas, + GetPageSizeForContents(), + GetPageSizeForAssets(), + resolver.Resolve()); + }); + } + + private int GetPageSizeForContents() + { + return resolver.Resolve>().Value.DefaultPageSizeGraphQl; + } + + private int GetPageSizeForAssets() + { + return resolver.Resolve>().Value.DefaultPageSizeGraphQl; + } + + private static object CreateCacheKey(Guid appId, string etag) + { + return $"GraphQLModel_{appId}_{etag}"; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs new file mode 100644 index 000000000..db16f194d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using GraphQL; +using GraphQL.DataLoader; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; +using Squidex.Domain.Apps.Entities.Contents.Queries; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public sealed class GraphQLExecutionContext : QueryExecutionContext + { + private static readonly List EmptyAssets = new List(); + private static readonly List EmptyContents = new List(); + private readonly IDataLoaderContextAccessor dataLoaderContextAccessor; + private readonly IDependencyResolver resolver; + + public IGraphQLUrlGenerator UrlGenerator { get; } + + public ISemanticLog Log { get; } + + public GraphQLExecutionContext(Context context, IDependencyResolver resolver) + : base(context, + resolver.Resolve(), + resolver.Resolve()) + { + UrlGenerator = resolver.Resolve(); + + dataLoaderContextAccessor = resolver.Resolve(); + + this.resolver = resolver; + } + + public void Setup(ExecutionOptions execution) + { + var loader = resolver.Resolve(); + + execution.Listeners.Add(loader); + execution.FieldMiddleware.Use(Middlewares.Logging(resolver.Resolve())); + execution.FieldMiddleware.Use(Middlewares.Errors()); + + execution.UserContext = this; + } + + public override async Task FindAssetAsync(Guid id) + { + var dataLoader = GetAssetsLoader(); + + return await dataLoader.LoadAsync(id); + } + + public async Task FindContentAsync(Guid id) + { + var dataLoader = GetContentsLoader(); + + return await dataLoader.LoadAsync(id); + } + + public async Task> GetReferencedAssetsAsync(IJsonValue value) + { + var ids = ParseIds(value); + + if (ids == null) + { + return EmptyAssets; + } + + var dataLoader = GetAssetsLoader(); + + return await dataLoader.LoadManyAsync(ids); + } + + public async Task> GetReferencedContentsAsync(IJsonValue value) + { + var ids = ParseIds(value); + + if (ids == null) + { + return EmptyContents; + } + + var dataLoader = GetContentsLoader(); + + return await dataLoader.LoadManyAsync(ids); + } + + private IDataLoader GetAssetsLoader() + { + return dataLoaderContextAccessor.Context.GetOrAddBatchLoader("Assets", + async batch => + { + var result = await GetReferencedAssetsAsync(new List(batch)); + + return result.ToDictionary(x => x.Id); + }); + } + + private IDataLoader GetContentsLoader() + { + return dataLoaderContextAccessor.Context.GetOrAddBatchLoader($"References", + async batch => + { + var result = await GetReferencedContentsAsync(new List(batch)); + + return result.ToDictionary(x => x.Id); + }); + } + + private static ICollection? ParseIds(IJsonValue value) + { + try + { + var result = new List(); + + if (value is JsonArray array) + { + foreach (var id in array) + { + result.Add(Guid.Parse(id.ToString())); + } + } + + return result; + } + catch + { + return null; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs new file mode 100644 index 000000000..4f27ea7ce --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs @@ -0,0 +1,180 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using GraphQL; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using GraphQLSchema = GraphQL.Types.Schema; + +#pragma warning disable IDE0003 + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public sealed class GraphQLModel : IGraphModel + { + private readonly Dictionary contentTypes = new Dictionary(); + private readonly PartitionResolver partitionResolver; + private readonly IAppEntity app; + private readonly IObjectGraphType assetType; + private readonly IGraphType assetListType; + private readonly GraphQLSchema graphQLSchema; + + public bool CanGenerateAssetSourceUrl { get; } + + public GraphQLModel(IAppEntity app, + IEnumerable schemas, + int pageSizeContents, + int pageSizeAssets, + IGraphQLUrlGenerator urlGenerator) + { + this.app = app; + + partitionResolver = app.PartitionResolver(); + + CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl; + + assetType = new AssetGraphType(this); + assetListType = new ListGraphType(new NonNullGraphType(assetType)); + + var allSchemas = schemas.Where(x => x.SchemaDef.IsPublished).ToList(); + + BuildSchemas(allSchemas); + + graphQLSchema = BuildSchema(this, pageSizeContents, pageSizeAssets, allSchemas); + graphQLSchema.RegisterValueConverter(JsonConverter.Instance); + + InitializeContentTypes(); + } + + private void BuildSchemas(List allSchemas) + { + foreach (var schema in allSchemas) + { + contentTypes[schema.Id] = new ContentGraphType(schema); + } + } + + private void InitializeContentTypes() + { + foreach (var contentType in contentTypes.Values) + { + contentType.Initialize(this); + } + + foreach (var contentType in contentTypes.Values) + { + graphQLSchema.RegisterType(contentType); + } + } + + private static GraphQLSchema BuildSchema(GraphQLModel model, int pageSizeContents, int pageSizeAssets, List schemas) + { + var schema = new GraphQLSchema + { + Query = new AppQueriesGraphType(model, pageSizeContents, pageSizeAssets, schemas) + }; + + return schema; + } + + public IFieldResolver ResolveAssetUrl() + { + var resolver = new FuncFieldResolver(c => + { + var context = (GraphQLExecutionContext)c.UserContext; + + return context.UrlGenerator.GenerateAssetUrl(app, c.Source); + }); + + return resolver; + } + + public IFieldResolver ResolveAssetSourceUrl() + { + var resolver = new FuncFieldResolver(c => + { + var context = (GraphQLExecutionContext)c.UserContext; + + return context.UrlGenerator.GenerateAssetSourceUrl(app, c.Source); + }); + + return resolver; + } + + public IFieldResolver ResolveAssetThumbnailUrl() + { + var resolver = new FuncFieldResolver(c => + { + var context = (GraphQLExecutionContext)c.UserContext; + + return context.UrlGenerator.GenerateAssetThumbnailUrl(app, c.Source); + }); + + return resolver; + } + + public IFieldResolver ResolveContentUrl(ISchemaEntity schema) + { + var resolver = new FuncFieldResolver(c => + { + var context = (GraphQLExecutionContext)c.UserContext; + + return context.UrlGenerator.GenerateContentUrl(app, schema, c.Source); + }); + + return resolver; + } + + public IFieldPartitioning ResolvePartition(Partitioning key) + { + return partitionResolver(key); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName) + { + return field.Accept(new QueryGraphTypeVisitor(schema, contentTypes, this, assetListType, fieldName)); + } + + public IObjectGraphType GetAssetType() + { + return assetType as IObjectGraphType; + } + + public IObjectGraphType GetContentType(Guid schemaId) + { + return contentTypes.GetOrDefault(schemaId); + } + + public async Task<(object Data, object[]? Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query) + { + Guard.NotNull(context, nameof(context)); + + var result = await new DocumentExecuter().ExecuteAsync(execution => + { + context.Setup(execution); + + execution.Schema = graphQLSchema; + execution.Inputs = query.Variables?.ToInputs(); + execution.Query = query.Query; + }).ConfigureAwait(false); + + return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs new file mode 100644 index 000000000..eabe2f8b8 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public interface IGraphModel + { + bool CanGenerateAssetSourceUrl { get; } + + IFieldPartitioning ResolvePartition(Partitioning key); + + IFieldResolver ResolveAssetUrl(); + + IFieldResolver ResolveAssetSourceUrl(); + + IFieldResolver ResolveAssetThumbnailUrl(); + + IFieldResolver ResolveContentUrl(ISchemaEntity schema); + + IObjectGraphType GetAssetType(); + + IObjectGraphType GetContentType(Guid schemaId); + + (IGraphType? ResolveType, ValueResolver? Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs new file mode 100644 index 000000000..8be64d064 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public interface IGraphQLUrlGenerator + { + bool CanGenerateAssetSourceUrl { get; } + + string? GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset); + + string? GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset); + + string GenerateAssetUrl(IAppEntity app, IAssetEntity asset); + + string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs new file mode 100644 index 000000000..86bd5e74f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using GraphQL; +using GraphQL.Instrumentation; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public static class Middlewares + { + public static Func Logging(ISemanticLog log) + { + Guard.NotNull(log); + + return next => + { + return async context => + { + try + { + return await next(context); + } + catch (Exception ex) + { + log.LogWarning(ex, w => w + .WriteProperty("action", "reolveField") + .WriteProperty("status", "failed") + .WriteProperty("field", context.FieldName)); + + throw; + } + }; + }; + } + + public static Func Errors() + { + return next => + { + return async context => + { + try + { + return await next(context); + } + catch (DomainException ex) + { + throw new ExecutionError(ex.Message); + } + }; + }; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs new file mode 100644 index 000000000..6dde0c175 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs @@ -0,0 +1,194 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class AssetGraphType : ObjectGraphType + { + public AssetGraphType(IGraphModel model) + { + Name = "Asset"; + + AddField(new FieldType + { + Name = "id", + ResolvedType = AllTypes.NonNullGuid, + Resolver = Resolve(x => x.Id.ToString()), + Description = "The id of the asset." + }); + + AddField(new FieldType + { + Name = "version", + ResolvedType = AllTypes.NonNullInt, + Resolver = Resolve(x => x.Version), + Description = "The version of the asset." + }); + + AddField(new FieldType + { + Name = "created", + ResolvedType = AllTypes.NonNullDate, + Resolver = Resolve(x => x.Created), + Description = "The date and time when the asset has been created." + }); + + AddField(new FieldType + { + Name = "createdBy", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.CreatedBy.ToString()), + Description = "The user that has created the asset." + }); + + AddField(new FieldType + { + Name = "lastModified", + ResolvedType = AllTypes.NonNullDate, + Resolver = Resolve(x => x.LastModified), + Description = "The date and time when the asset has been modified last." + }); + + AddField(new FieldType + { + Name = "lastModifiedBy", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.LastModifiedBy.ToString()), + Description = "The user that has updated the asset last." + }); + + AddField(new FieldType + { + Name = "mimeType", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.MimeType), + Description = "The mime type." + }); + + AddField(new FieldType + { + Name = "url", + ResolvedType = AllTypes.NonNullString, + Resolver = model.ResolveAssetUrl(), + Description = "The url to the asset." + }); + + AddField(new FieldType + { + Name = "thumbnailUrl", + ResolvedType = AllTypes.String, + Resolver = model.ResolveAssetThumbnailUrl(), + Description = "The thumbnail url to the asset." + }); + + AddField(new FieldType + { + Name = "fileName", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.FileName), + Description = "The file name." + }); + + AddField(new FieldType + { + Name = "fileHash", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.FileHash), + Description = "The hash of the file. Can be null for old files." + }); + + AddField(new FieldType + { + Name = "fileType", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.FileName.FileType()), + Description = "The file type." + }); + + AddField(new FieldType + { + Name = "fileSize", + ResolvedType = AllTypes.NonNullInt, + Resolver = Resolve(x => x.FileSize), + Description = "The size of the file in bytes." + }); + + AddField(new FieldType + { + Name = "fileVersion", + ResolvedType = AllTypes.NonNullInt, + Resolver = Resolve(x => x.FileVersion), + Description = "The version of the file." + }); + + AddField(new FieldType + { + Name = "slug", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.Slug), + Description = "The file name as slug." + }); + + AddField(new FieldType + { + Name = "isImage", + ResolvedType = AllTypes.NonNullBoolean, + Resolver = Resolve(x => x.IsImage), + Description = "Determines of the created file is an image." + }); + + AddField(new FieldType + { + Name = "pixelWidth", + ResolvedType = AllTypes.Int, + Resolver = Resolve(x => x.PixelWidth), + Description = "The width of the image in pixels if the asset is an image." + }); + + AddField(new FieldType + { + Name = "pixelHeight", + ResolvedType = AllTypes.Int, + Resolver = Resolve(x => x.PixelHeight), + Description = "The height of the image in pixels if the asset is an image." + }); + + AddField(new FieldType + { + Name = "tags", + ResolvedType = null, + Resolver = Resolve(x => x.TagNames), + Description = "The asset tags.", + Type = AllTypes.NonNullTagsType + }); + + if (model.CanGenerateAssetSourceUrl) + { + AddField(new FieldType + { + Name = "sourceUrl", + ResolvedType = AllTypes.NonNullString, + Resolver = model.ResolveAssetSourceUrl(), + Description = "The source url of the asset." + }); + } + + Description = "An asset"; + } + + private static IFieldResolver Resolve(Func action) + { + return new FuncFieldResolver(c => action(c.Source)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs new file mode 100644 index 000000000..97e299469 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs @@ -0,0 +1,91 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class ContentDataGraphType : ObjectGraphType + { + public ContentDataGraphType(ISchemaEntity schema, string schemaName, string schemaType, IGraphModel model) + { + Name = $"{schemaType}DataDto"; + + foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields()) + { + var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName); + + if (valueResolver != null) + { + var displayName = field.DisplayName(); + + var fieldGraphType = new ObjectGraphType + { + Name = $"{schemaType}Data{typeName}Dto" + }; + + var partition = model.ResolvePartition(field.Partitioning); + + foreach (var partitionItem in partition) + { + var key = partitionItem.Key; + + fieldGraphType.AddField(new FieldType + { + Name = key.EscapePartition(), + Resolver = PartitionResolver(valueResolver, key), + ResolvedType = resolvedType, + Description = field.RawProperties.Hints + }); + } + + fieldGraphType.Description = $"The structure of the {displayName} field of the {schemaName} content type."; + + AddField(new FieldType + { + Name = fieldName, + Resolver = FieldResolver(field), + ResolvedType = fieldGraphType, + Description = $"The {displayName} field." + }); + } + } + + Description = $"The structure of the {schemaName} content type."; + } + + private static FuncFieldResolver PartitionResolver(ValueResolver valueResolver, string key) + { + return new FuncFieldResolver(c => + { + if (((ContentFieldData)c.Source).TryGetValue(key, out var value) && value != null) + { + return valueResolver(value, c); + } + else + { + return null; + } + }); + } + + private static FuncFieldResolver?> FieldResolver(RootField field) + { + return new FuncFieldResolver?>(c => + { + return c.Source?.GetOrDefault(field.Name); + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs new file mode 100644 index 000000000..b0140d917 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs @@ -0,0 +1,144 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class ContentGraphType : ObjectGraphType + { + private readonly ISchemaEntity schema; + private readonly string schemaType; + private readonly string schemaName; + + public ContentGraphType(ISchemaEntity schema) + { + this.schema = schema; + + schemaType = schema.TypeName(); + schemaName = schema.DisplayName(); + + Name = $"{schemaType}"; + + AddField(new FieldType + { + Name = "id", + ResolvedType = AllTypes.NonNullGuid, + Resolver = Resolve(x => x.Id), + Description = $"The id of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "version", + ResolvedType = AllTypes.NonNullInt, + Resolver = Resolve(x => x.Version), + Description = $"The version of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "created", + ResolvedType = AllTypes.NonNullDate, + Resolver = Resolve(x => x.Created), + Description = $"The date and time when the {schemaName} content has been created." + }); + + AddField(new FieldType + { + Name = "createdBy", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.CreatedBy.ToString()), + Description = $"The user that has created the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "lastModified", + ResolvedType = AllTypes.NonNullDate, + Resolver = Resolve(x => x.LastModified), + Description = $"The date and time when the {schemaName} content has been modified last." + }); + + AddField(new FieldType + { + Name = "lastModifiedBy", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.LastModifiedBy.ToString()), + Description = $"The user that has updated the {schemaName} content last." + }); + + AddField(new FieldType + { + Name = "status", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.Status.Name.ToUpperInvariant()), + Description = $"The the status of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "statusColor", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.StatusColor), + Description = $"The color status of the {schemaName} content." + }); + + Interface(); + + Description = $"The structure of a {schemaName} content type."; + + IsTypeOf = CheckType; + } + + private bool CheckType(object value) + { + return value is IContentEntity content && content.SchemaId?.Id == schema.Id; + } + + public void Initialize(IGraphModel model) + { + AddField(new FieldType + { + Name = "url", + ResolvedType = AllTypes.NonNullString, + Resolver = model.ResolveContentUrl(schema), + Description = $"The url to the the {schemaName} content." + }); + + var contentDataType = new ContentDataGraphType(schema, schemaName, schemaType, model); + + if (contentDataType.Fields.Any()) + { + AddField(new FieldType + { + Name = "data", + ResolvedType = new NonNullGraphType(contentDataType), + Resolver = Resolve(x => x.Data), + Description = $"The data of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "dataDraft", + ResolvedType = contentDataType, + Resolver = Resolve(x => x.DataDraft), + Description = $"The draft data of the {schemaName} content." + }); + } + } + + private static IFieldResolver Resolve(Func action) + { + return new FuncFieldResolver(c => action(c.Source)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs new file mode 100644 index 000000000..53b92df13 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs @@ -0,0 +1,60 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using GraphQL.Types; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class ContentUnionGraphType : UnionGraphType + { + private readonly Dictionary types = new Dictionary(); + + public ContentUnionGraphType(string fieldName, Dictionary schemaTypes, IEnumerable? schemaIds) + { + Name = $"{fieldName}ReferenceUnionDto"; + + if (schemaIds?.Any() == true) + { + foreach (var schemaId in schemaIds) + { + var schemaType = schemaTypes.GetOrDefault(schemaId); + + if (schemaType != null) + { + types[schemaId] = schemaType; + } + } + } + else + { + foreach (var schemaType in schemaTypes) + { + types[schemaType.Key] = schemaType.Value; + } + } + + foreach (var type in types) + { + AddPossibleType(type.Value); + } + + ResolveType = value => + { + if (value is IContentEntity content) + { + return types.GetOrDefault(content.SchemaId.Id); + } + + return null; + }; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs new file mode 100644 index 000000000..7f90e1f52 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class NestedGraphType : ObjectGraphType + { + public NestedGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field, string fieldName) + { + var schemaType = schema.TypeName(); + var schemaName = schema.DisplayName(); + + var fieldDisplayName = field.DisplayName(); + + Name = $"{schemaType}{fieldName}ChildDto"; + + foreach (var (nestedField, nestedName, _) in field.Fields.SafeFields()) + { + var fieldInfo = model.GetGraphType(schema, nestedField, nestedName); + + if (fieldInfo.ResolveType != null && fieldInfo.Resolver != null) + { + var resolver = ValueResolver(nestedField, fieldInfo.Resolver); + + AddField(new FieldType + { + Name = nestedName, + Resolver = resolver, + ResolvedType = fieldInfo.ResolveType, + Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field." + }); + } + } + + Description = $"The structure of the {schemaName}.{fieldDisplayName} nested schema."; + } + + private static FuncFieldResolver ValueResolver(NestedField nestedField, ValueResolver resolver) + { + return new FuncFieldResolver(c => + { + if (((JsonObject)c.Source).TryGetValue(nestedField.Name, out var value)) + { + return resolver(value, c); + } + else + { + return null; + } + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs new file mode 100644 index 000000000..5867a7f07 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs @@ -0,0 +1,150 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using GraphQL.Types; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public delegate object ValueResolver(IJsonValue value, ResolveFieldContext context); + + public sealed class QueryGraphTypeVisitor : IFieldVisitor<(IGraphType? ResolveType, ValueResolver? Resolver)> + { + private static readonly ValueResolver NoopResolver = (value, c) => value; + private readonly Dictionary schemaTypes; + private readonly ISchemaEntity schema; + private readonly IGraphModel model; + private readonly IGraphType assetListType; + private readonly string fieldName; + + public QueryGraphTypeVisitor(ISchemaEntity schema, + Dictionary schemaTypes, + IGraphModel model, + IGraphType assetListType, + string fieldName) + { + this.model = model; + this.assetListType = assetListType; + this.schema = schema; + this.schemaTypes = schemaTypes; + this.fieldName = fieldName; + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IArrayField field) + { + return ResolveNested(field); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveAssets(); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveDefault(AllTypes.NoopBoolean); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveDefault(AllTypes.NoopDate); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveDefault(AllTypes.NoopGeolocation); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveDefault(AllTypes.NoopJson); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveDefault(AllTypes.NoopFloat); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveReferences(field); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveDefault(AllTypes.NoopString); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveDefault(AllTypes.NoopTags); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return (null, null); + } + + private static (IGraphType? ResolveType, ValueResolver? Resolver) ResolveDefault(IGraphType type) + { + return (type, NoopResolver); + } + + private (IGraphType? ResolveType, ValueResolver? Resolver) ResolveNested(IArrayField field) + { + var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedGraphType(model, schema, field, fieldName))); + + return (schemaFieldType, NoopResolver); + } + + private (IGraphType? ResolveType, ValueResolver? Resolver) ResolveAssets() + { + var resolver = new ValueResolver((value, c) => + { + var context = (GraphQLExecutionContext)c.UserContext; + + return context.GetReferencedAssetsAsync(value); + }); + + return (assetListType, resolver); + } + + private (IGraphType? ResolveType, ValueResolver? Resolver) ResolveReferences(IField field) + { + IGraphType contentType = schemaTypes.GetOrDefault(field.Properties.SingleId()); + + if (contentType == null) + { + var union = new ContentUnionGraphType(fieldName, schemaTypes, field.Properties.SchemaIds); + + if (!union.PossibleTypes.Any()) + { + return (null, null); + } + + contentType = union; + } + + var resolver = new ValueResolver((value, c) => + { + var context = (GraphQLExecutionContext)c.UserContext; + + return context.GetReferencedContentsAsync(value); + }); + + var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType)); + + return (schemaFieldType, resolver); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs new file mode 100644 index 000000000..a19703dbd --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using GraphQL.Language.AST; +using GraphQL.Types; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +{ + public sealed class GuidGraphType2 : ScalarGraphType + { + public GuidGraphType2() + { + Name = "Guid"; + + Description = "The `Guid` scalar type global unique identifier"; + } + + public override object? Serialize(object value) + { + return ParseValue(value)?.ToString(); + } + + public override object? ParseValue(object value) + { + if (value is Guid guid) + { + return guid; + } + + var inputValue = value?.ToString()?.Trim('"'); + + if (Guid.TryParse(inputValue, out guid)) + { + return guid; + } + + return null; + } + + public override object? ParseLiteral(IValue value) + { + if (value is StringValue stringValue) + { + return ParseValue(stringValue.Value); + } + + return null; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs new file mode 100644 index 000000000..bec2558a2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Language.AST; +using GraphQL.Types; +using NodaTime.Text; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +{ + public sealed class InstantGraphType : DateGraphType + { + public override object Serialize(object value) + { + return ParseValue(value); + } + + public override object ParseValue(object value) + { + return InstantPattern.General.Parse(value.ToString()).Value; + } + + public override object? ParseLiteral(IValue value) + { + if (value is InstantValue timeValue) + { + return ParseValue(timeValue.Value); + } + + if (value is StringValue stringValue) + { + return ParseValue(stringValue.Value); + } + + return null; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValue.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValue.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValue.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValue.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs new file mode 100644 index 000000000..ea597a4b3 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Language.AST; +using GraphQL.Types; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +{ + public sealed class JsonConverter : IAstFromValueConverter + { + public static readonly JsonConverter Instance = new JsonConverter(); + + private JsonConverter() + { + } + + public IValue Convert(object value, IGraphType type) + { + return new JsonValueNode(value as JsonObject ?? JsonValue.Null); + } + + public bool Matches(object value, IGraphType type) + { + return value is JsonObject; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs new file mode 100644 index 000000000..6540fd92a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Language.AST; +using GraphQL.Types; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +{ + public sealed class JsonGraphType : ScalarGraphType + { + public JsonGraphType() + { + Name = "Json"; + + Description = "Unstructured Json object"; + } + + public override object Serialize(object value) + { + return value; + } + + public override object ParseValue(object value) + { + return value; + } + + public override object ParseLiteral(IValue value) + { + if (value is JsonValueNode jsonGraphType) + { + return jsonGraphType.Value; + } + + return value; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValueNode.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValueNode.cs new file mode 100644 index 000000000..7ac033df6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValueNode.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Language.AST; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +{ + public sealed class JsonValueNode : ValueNode + { + public JsonValueNode(IJsonValue value) + { + Value = value; + } + + protected override bool Equals(ValueNode node) + { + return false; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs new file mode 100644 index 000000000..9e346fe42 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -0,0 +1,136 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Contents.Guards +{ + public static class GuardContent + { + public static async Task CanCreate(ISchemaEntity schema, IContentWorkflow contentWorkflow, CreateContent command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot created content.", e => + { + ValidateData(command, e); + }); + + if (schema.SchemaDef.IsSingleton && command.ContentId != schema.Id) + { + throw new DomainException("Singleton content cannot be created."); + } + + if (command.Publish && !await contentWorkflow.CanPublishOnCreateAsync(schema, command.Data, command.User)) + { + throw new DomainException("Content workflow prevents publishing."); + } + } + + public static async Task CanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, UpdateContent command, bool isProposal) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot update content.", e => + { + ValidateData(command, e); + }); + + if (!isProposal) + { + await ValidateCanUpdate(content, contentWorkflow); + } + } + + public static async Task CanPatch(IContentEntity content, IContentWorkflow contentWorkflow, PatchContent command, bool isProposal) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot patch content.", e => + { + ValidateData(command, e); + }); + + if (!isProposal) + { + await ValidateCanUpdate(content, contentWorkflow); + } + } + + public static void CanDiscardChanges(bool isPending, DiscardChanges command) + { + Guard.NotNull(command); + + if (!isPending) + { + throw new DomainException("The content has no pending changes."); + } + } + + public static Task CanChangeStatus(ISchemaEntity schema, IContentEntity content, IContentWorkflow contentWorkflow, ChangeContentStatus command, bool isChangeConfirm) + { + Guard.NotNull(command); + + if (schema.SchemaDef.IsSingleton && command.Status != Status.Published) + { + throw new DomainException("Singleton content cannot be changed."); + } + + return Validate.It(() => "Cannot change status.", async e => + { + if (isChangeConfirm) + { + if (!content.IsPending) + { + e("Content has no changes to publish.", nameof(command.Status)); + } + } + else if (!await contentWorkflow.CanMoveToAsync(content, command.Status, command.User)) + { + e($"Cannot change status from {content.Status} to {command.Status}.", nameof(command.Status)); + } + + if (command.DueTime.HasValue && command.DueTime.Value < SystemClock.Instance.GetCurrentInstant()) + { + e("Due time must be in the future.", nameof(command.DueTime)); + } + }); + } + + public static void CanDelete(ISchemaEntity schema, DeleteContent command) + { + Guard.NotNull(command); + + if (schema.SchemaDef.IsSingleton) + { + throw new DomainException("Singleton content cannot be deleted."); + } + } + + private static void ValidateData(ContentDataCommand command, AddValidation e) + { + if (command.Data == null) + { + e(Not.Defined("Data"), nameof(command.Data)); + } + } + + private static async Task ValidateCanUpdate(IContentEntity content, IContentWorkflow contentWorkflow) + { + if (!await contentWorkflow.CanUpdateAsync(content)) + { + throw new DomainException($"The workflow does not allow updates at status {content.Status}"); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs new file mode 100644 index 000000000..ed1919e63 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public interface IContentEntity : + IEntity, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion + { + NamedId AppId { get; } + + NamedId SchemaId { get; } + + Status Status { get; } + + ScheduleJob? ScheduleJob { get; } + + NamedContentData? Data { get; } + + NamedContentData DataDraft { get; } + + bool IsPending { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs new file mode 100644 index 000000000..618881ff2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.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.Contents; +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public interface IEnrichedContentEntity : IContentEntity, IEntityWithCacheDependencies + { + bool CanUpdate { get; } + + string StatusColor { get; } + + string SchemaName { get; } + + string SchemaDisplayName { get; } + + RootField[]? ReferenceFields { get; } + + StatusInfo[]? Nexts { get; } + + NamedContentData? ReferenceData { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs new file mode 100644 index 000000000..fce76d51e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs @@ -0,0 +1,377 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.ExtractReferenceIds; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public sealed class ContentEnricher : IContentEnricher + { + private const string DefaultColor = StatusColors.Draft; + private static readonly ILookup EmptyContents = Enumerable.Empty().ToLookup(x => x.Id); + private static readonly ILookup EmptyAssets = Enumerable.Empty().ToLookup(x => x.Id); + private readonly IAssetQueryService assetQuery; + private readonly IAssetUrlGenerator assetUrlGenerator; + private readonly Lazy contentQuery; + private readonly IContentWorkflow contentWorkflow; + + private IContentQueryService ContentQuery + { + get { return contentQuery.Value; } + } + + public ContentEnricher(IAssetQueryService assetQuery, IAssetUrlGenerator assetUrlGenerator, Lazy contentQuery, IContentWorkflow contentWorkflow) + { + Guard.NotNull(assetQuery, nameof(assetQuery)); + Guard.NotNull(assetUrlGenerator, nameof(assetUrlGenerator)); + Guard.NotNull(contentQuery, nameof(contentQuery)); + Guard.NotNull(contentWorkflow, nameof(contentWorkflow)); + + this.assetQuery = assetQuery; + this.assetUrlGenerator = assetUrlGenerator; + this.contentQuery = contentQuery; + this.contentWorkflow = contentWorkflow; + } + + public async Task EnrichAsync(IContentEntity content, Context context) + { + Guard.NotNull(content, nameof(content)); + + var enriched = await EnrichAsync(Enumerable.Repeat(content, 1), context); + + return enriched[0]; + } + + public async Task> EnrichAsync(IEnumerable contents, Context context) + { + Guard.NotNull(contents, nameof(contents)); + Guard.NotNull(context, nameof(context)); + + using (Profiler.TraceMethod()) + { + var results = new List(); + + if (contents.Any()) + { + var appVersion = context.App.Version; + + var cache = new Dictionary<(Guid, Status), StatusInfo>(); + + foreach (var content in contents) + { + var result = SimpleMapper.Map(content, new ContentEntity()); + + await EnrichColorAsync(content, result, cache); + + if (ShouldEnrichWithStatuses(context)) + { + await EnrichNextsAsync(content, result, context); + await EnrichCanUpdateAsync(content, result); + } + + results.Add(result); + } + + foreach (var group in results.GroupBy(x => x.SchemaId.Id)) + { + var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); + + foreach (var content in group) + { + content.CacheDependencies = new HashSet + { + schema.Id, + schema.Version + }; + } + + if (ShouldEnrichWithSchema(context)) + { + var referenceFields = schema.SchemaDef.ReferenceFields().ToArray(); + + var schemaName = schema.SchemaDef.Name; + var schemaDisplayName = schema.SchemaDef.DisplayNameUnchanged(); + + foreach (var content in group) + { + content.ReferenceFields = referenceFields; + content.SchemaName = schemaName; + content.SchemaDisplayName = schemaDisplayName; + } + } + } + + if (ShouldEnrich(context)) + { + await EnrichReferencesAsync(context, results); + await EnrichAssetsAsync(context, results); + } + } + + return results; + } + } + + private async Task EnrichAssetsAsync(Context context, List contents) + { + var ids = new HashSet(); + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); + + AddAssetIds(ids, schema, group); + } + + var assets = await GetAssetsAsync(context, ids); + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); + + ResolveAssets(schema, group, assets); + } + } + + private async Task EnrichReferencesAsync(Context context, List contents) + { + var ids = new HashSet(); + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); + + AddReferenceIds(ids, schema, group); + } + + var references = await GetReferencesAsync(context, ids); + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); + + await ResolveReferencesAsync(context, schema, group, references); + } + } + + private async Task ResolveReferencesAsync(Context context, ISchemaEntity schema, IEnumerable contents, ILookup references) + { + var formatted = new Dictionary(); + + foreach (var field in schema.SchemaDef.ResolvingReferences()) + { + foreach (var content in contents) + { + if (content.ReferenceData == null) + { + content.ReferenceData = new NamedContentData(); + } + + var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; + + try + { + if (content.DataDraft.TryGetValue(field.Name, out var fieldData) && fieldData != null) + { + foreach (var partitionValue in fieldData) + { + var referencedContents = + field.GetReferencedIds(partitionValue.Value, Ids.ContentOnly) + .Select(x => references[x]) + .SelectMany(x => x) + .ToList(); + + if (referencedContents.Count == 1) + { + var reference = referencedContents[0]; + + var referencedSchema = await ContentQuery.GetSchemaOrThrowAsync(context, reference.SchemaId.Id.ToString()); + + content.CacheDependencies.Add(referencedSchema.Id); + content.CacheDependencies.Add(referencedSchema.Version); + content.CacheDependencies.Add(reference.Id); + content.CacheDependencies.Add(reference.Version); + + var value = formatted.GetOrAdd(reference, x => Format(x, context, referencedSchema)); + + fieldReference.AddJsonValue(partitionValue.Key, value); + } + else if (referencedContents.Count > 1) + { + var value = CreateFallback(context, referencedContents); + + fieldReference.AddJsonValue(partitionValue.Key, value); + } + } + } + } + catch (DomainObjectNotFoundException) + { + continue; + } + } + } + } + + private void ResolveAssets(ISchemaEntity schema, IGrouping contents, ILookup assets) + { + foreach (var field in schema.SchemaDef.ResolvingAssets()) + { + foreach (var content in contents) + { + if (content.ReferenceData == null) + { + content.ReferenceData = new NamedContentData(); + } + + var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; + + if (content.DataDraft.TryGetValue(field.Name, out var fieldData) && fieldData != null) + { + foreach (var partitionValue in fieldData) + { + var referencedImage = + field.GetReferencedIds(partitionValue.Value, Ids.ContentOnly) + .Select(x => assets[x]) + .SelectMany(x => x) + .FirstOrDefault(x => x.IsImage); + + if (referencedImage != null) + { + var url = assetUrlGenerator.GenerateUrl(referencedImage.Id.ToString()); + + content.CacheDependencies.Add(referencedImage.Id); + content.CacheDependencies.Add(referencedImage.Version); + + fieldReference.AddJsonValue(partitionValue.Key, JsonValue.Create(url)); + } + } + } + } + } + } + + private static JsonObject Format(IContentEntity content, Context context, ISchemaEntity referencedSchema) + { + return content.DataDraft.FormatReferences(referencedSchema.SchemaDef, context.App.LanguagesConfig); + } + + private static JsonObject CreateFallback(Context context, List referencedContents) + { + var text = $"{referencedContents.Count} Reference(s)"; + + var value = JsonValue.Object(); + + foreach (var language in context.App.LanguagesConfig) + { + value.Add(language.Key, text); + } + + return value; + } + + private void AddReferenceIds(HashSet ids, ISchemaEntity schema, IEnumerable contents) + { + foreach (var content in contents) + { + ids.AddRange(content.DataDraft.GetReferencedIds(schema.SchemaDef.ResolvingReferences(), Ids.ContentOnly)); + } + } + + private void AddAssetIds(HashSet ids, ISchemaEntity schema, IEnumerable contents) + { + foreach (var content in contents) + { + ids.AddRange(content.DataDraft.GetReferencedIds(schema.SchemaDef.ResolvingAssets(), Ids.ContentOnly)); + } + } + + private async Task> GetReferencesAsync(Context context, HashSet ids) + { + if (ids.Count == 0) + { + return EmptyContents; + } + + var references = await ContentQuery.QueryAsync(context.Clone().WithNoContentEnrichment(true), ids.ToList()); + + return references.ToLookup(x => x.Id); + } + + private async Task> GetAssetsAsync(Context context, HashSet ids) + { + if (ids.Count == 0) + { + return EmptyAssets; + } + + var assets = await assetQuery.QueryAsync(context.Clone().WithNoAssetEnrichment(true), Q.Empty.WithIds(ids)); + + return assets.ToLookup(x => x.Id); + } + + private async Task EnrichCanUpdateAsync(IContentEntity content, ContentEntity result) + { + result.CanUpdate = await contentWorkflow.CanUpdateAsync(content); + } + + private async Task EnrichNextsAsync(IContentEntity content, ContentEntity result, Context context) + { + result.Nexts = await contentWorkflow.GetNextsAsync(content, context.User); + } + + private async Task EnrichColorAsync(IContentEntity content, ContentEntity result, Dictionary<(Guid, Status), StatusInfo> cache) + { + result.StatusColor = await GetColorAsync(content, cache); + } + + private async Task GetColorAsync(IContentEntity content, Dictionary<(Guid, Status), StatusInfo> cache) + { + if (!cache.TryGetValue((content.SchemaId.Id, content.Status), out var info)) + { + info = await contentWorkflow.GetInfoAsync(content); + + if (info == null) + { + info = new StatusInfo(content.Status, DefaultColor); + } + + cache[(content.SchemaId.Id, content.Status)] = info; + } + + return info.Color; + } + + private static bool ShouldEnrichWithSchema(Context context) + { + return context.IsFrontendClient; + } + + private static bool ShouldEnrichWithStatuses(Context context) + { + return context.IsFrontendClient || context.IsResolveFlow(); + } + + private static bool ShouldEnrich(Context context) + { + return context.IsFrontendClient && !context.IsNoEnrichment(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs new file mode 100644 index 000000000..47ff85b87 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public sealed class ContentLoader : IContentLoader + { + private readonly IGrainFactory grainFactory; + + public ContentLoader(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public async Task GetAsync(Guid id, long version) + { + using (Profiler.TraceMethod()) + { + var grain = grainFactory.GetGrain(id); + + var content = await grain.GetStateAsync(version); + + if (content.Value == null || (version > EtagVersion.Any && content.Value.Version != version)) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity)); + } + + return content.Value; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs new file mode 100644 index 000000000..6bdf754c2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs @@ -0,0 +1,205 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.OData; +using Microsoft.OData.Edm; +using NJsonSchema; +using Squidex.Domain.Apps.Core.GenerateEdmSchema; +using Squidex.Domain.Apps.Core.GenerateJsonSchema; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.Queries.OData; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class ContentQueryParser : CachingProviderBase + { + private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); + private readonly IJsonSerializer jsonSerializer; + private readonly ContentOptions options; + + public ContentQueryParser(IMemoryCache cache, IJsonSerializer jsonSerializer, IOptions options) + : base(cache) + { + this.jsonSerializer = jsonSerializer; + this.options = options.Value; + } + + public virtual ClrQuery ParseQuery(Context context, ISchemaEntity schema, Q q) + { + Guard.NotNull(context); + Guard.NotNull(schema); + + using (Profiler.TraceMethod()) + { + var result = new ClrQuery(); + + if (!string.IsNullOrWhiteSpace(q?.JsonQuery)) + { + result = ParseJson(context, schema, q.JsonQuery); + } + else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) + { + result = ParseOData(context, schema, q.ODataQuery); + } + + if (result.Sort.Count == 0) + { + result.Sort.Add(new SortNode(new List { "lastModified" }, SortOrder.Descending)); + } + + if (result.Take == long.MaxValue) + { + result.Take = options.DefaultPageSize; + } + else if (result.Take > options.MaxResults) + { + result.Take = options.MaxResults; + } + + return result; + } + } + + private ClrQuery ParseJson(Context context, ISchemaEntity schema, string json) + { + var jsonSchema = BuildJsonSchema(context, schema); + + return jsonSchema.Parse(json, jsonSerializer); + } + + private ClrQuery ParseOData(Context context, ISchemaEntity schema, string odata) + { + try + { + var model = BuildEdmModel(context, schema); + + return model.ParseQuery(odata).ToQuery(); + } + catch (NotSupportedException) + { + throw new ValidationException("OData operation is not supported."); + } + catch (ODataException ex) + { + throw new ValidationException($"Failed to parse query: {ex.Message}", ex); + } + } + + private JsonSchema BuildJsonSchema(Context context, ISchemaEntity schema) + { + var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient); + + var result = Cache.GetOrCreate(cacheKey, entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheTime; + + return BuildJsonSchema(schema.SchemaDef, context.App, context.IsFrontendClient); + }); + + return result; + } + + private IEdmModel BuildEdmModel(Context context, ISchemaEntity schema) + { + var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient); + + var result = Cache.GetOrCreate(cacheKey, entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheTime; + + return BuildEdmModel(schema.SchemaDef, context.App, context.IsFrontendClient); + }); + + return result; + } + + private static JsonSchema BuildJsonSchema(Schema schema, IAppEntity app, bool withHiddenFields) + { + var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), (n, s) => s, withHiddenFields); + + return new ContentSchemaBuilder().CreateContentSchema(schema, dataSchema); + } + + private static EdmModel BuildEdmModel(Schema schema, IAppEntity app, bool withHiddenFields) + { + var model = new EdmModel(); + + var pascalAppName = app.Name.ToPascalCase(); + var pascalSchemaName = schema.Name.ToPascalCase(); + + var typeFactory = new EdmTypeFactory(name => + { + var finalName = pascalSchemaName; + + if (!string.IsNullOrWhiteSpace(name)) + { + finalName += "."; + finalName += name; + } + + var result = model.SchemaElements.OfType().FirstOrDefault(x => x.Name == finalName); + + if (result != null) + { + return (result, false); + } + + result = new EdmComplexType(pascalAppName, finalName); + + model.AddElement(result); + + return (result, true); + }); + + var schemaType = schema.BuildEdmType(withHiddenFields, app.PartitionResolver(), typeFactory); + + var entityType = new EdmEntityType(app.Name.ToPascalCase(), schema.Name); + entityType.AddStructuralProperty(nameof(IContentEntity.Id).ToCamelCase(), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(IContentEntity.Created).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty(nameof(IContentEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(IContentEntity.LastModified).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty(nameof(IContentEntity.LastModifiedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(IContentEntity.Status).ToCamelCase(), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(IContentEntity.Version).ToCamelCase(), EdmPrimitiveTypeKind.Int32); + entityType.AddStructuralProperty(nameof(IContentEntity.Data).ToCamelCase(), new EdmComplexTypeReference(schemaType, false)); + + var container = new EdmEntityContainer("Squidex", "Container"); + + container.AddEntitySet("ContentSet", entityType); + + model.AddElement(container); + model.AddElement(schemaType); + model.AddElement(entityType); + + return model; + } + + private static string BuildEmdCacheKey(IAppEntity app, ISchemaEntity schema, bool withHidden) + { + return $"EDM/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; + } + + private static string BuildJsonCacheKey(IAppEntity app, ISchemaEntity schema, bool withHidden) + { + return $"JSON/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs new file mode 100644 index 000000000..bdd029137 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -0,0 +1,341 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared; + +#pragma warning disable RECS0147 + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public sealed class ContentQueryService : IContentQueryService + { + private static readonly Status[] StatusPublishedOnly = { Status.Published }; + private static readonly IResultList EmptyContents = ResultList.CreateFrom(0); + private readonly IAppProvider appProvider; + private readonly IAssetUrlGenerator assetUrlGenerator; + private readonly IContentEnricher contentEnricher; + private readonly IContentRepository contentRepository; + private readonly IContentLoader contentVersionLoader; + private readonly IScriptEngine scriptEngine; + private readonly ContentQueryParser queryParser; + + public ContentQueryService( + IAppProvider appProvider, + IAssetUrlGenerator assetUrlGenerator, + IContentEnricher contentEnricher, + IContentRepository contentRepository, + IContentLoader contentVersionLoader, + IScriptEngine scriptEngine, + ContentQueryParser queryParser) + { + Guard.NotNull(appProvider); + Guard.NotNull(assetUrlGenerator); + Guard.NotNull(contentEnricher); + Guard.NotNull(contentRepository); + Guard.NotNull(contentVersionLoader); + Guard.NotNull(queryParser); + Guard.NotNull(scriptEngine); + + this.appProvider = appProvider; + this.assetUrlGenerator = assetUrlGenerator; + this.contentEnricher = contentEnricher; + this.contentRepository = contentRepository; + this.contentVersionLoader = contentVersionLoader; + this.queryParser = queryParser; + this.scriptEngine = scriptEngine; + this.queryParser = queryParser; + } + + public async Task FindContentAsync(Context context, string schemaIdOrName, Guid id, long version = -1) + { + Guard.NotNull(context); + + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); + + CheckPermission(context, schema); + + using (Profiler.TraceMethod()) + { + IContentEntity? content; + + if (version > EtagVersion.Empty) + { + content = await FindByVersionAsync(id, version); + } + else + { + content = await FindCoreAsync(context, id, schema); + } + + if (content == null || content.SchemaId.Id != schema.Id) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity)); + } + + return await TransformAsync(context, schema, content); + } + } + + public async Task> QueryAsync(Context context, string schemaIdOrName, Q query) + { + Guard.NotNull(context); + + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); + + CheckPermission(context, schema); + + using (Profiler.TraceMethod()) + { + IResultList contents; + + if (query.Ids != null && query.Ids.Count > 0) + { + contents = await QueryByIdsAsync(context, schema, query); + } + else + { + contents = await QueryByQueryAsync(context, schema, query); + } + + return await TransformAsync(context, schema, contents); + } + } + + public async Task> QueryAsync(Context context, IReadOnlyList ids) + { + Guard.NotNull(context); + + using (Profiler.TraceMethod()) + { + if (ids == null || ids.Count == 0) + { + return EmptyContents; + } + + var results = new List(); + + var contents = await QueryCoreAsync(context, ids); + + foreach (var group in contents.GroupBy(x => x.Schema.Id)) + { + var schema = group.First().Schema; + + if (HasPermission(context, schema)) + { + var enriched = await TransformCoreAsync(context, schema, group.Select(x => x.Content)); + + results.AddRange(enriched); + } + } + + return ResultList.Create(results.Count, results.SortList(x => x.Id, ids)); + } + } + + private async Task> TransformAsync(Context context, ISchemaEntity schema, IResultList contents) + { + var transformed = await TransformCoreAsync(context, schema, contents); + + return ResultList.Create(contents.Total, transformed); + } + + private async Task TransformAsync(Context context, ISchemaEntity schema, IContentEntity content) + { + var transformed = await TransformCoreAsync(context, schema, Enumerable.Repeat(content, 1)); + + return transformed[0]; + } + + private async Task> TransformCoreAsync(Context context, ISchemaEntity schema, IEnumerable contents) + { + using (Profiler.TraceMethod()) + { + var results = new List(); + + var converters = GenerateConverters(context).ToArray(); + + var script = schema.SchemaDef.Scripts.Query; + var scripting = !string.IsNullOrWhiteSpace(script); + + var enriched = await contentEnricher.EnrichAsync(contents, context); + + foreach (var content in enriched) + { + var result = SimpleMapper.Map(content, new ContentEntity()); + + if (result.Data != null) + { + if (!context.IsFrontendClient && scripting) + { + var ctx = new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id }; + + result.Data = scriptEngine.Transform(ctx, script); + } + + result.Data = result.Data.ConvertName2Name(schema.SchemaDef, converters); + } + + if (result.DataDraft != null && (context.IsUnpublished() || context.IsFrontendClient)) + { + result.DataDraft = result.DataDraft.ConvertName2Name(schema.SchemaDef, converters); + } + else + { + result.DataDraft = null!; + } + + results.Add(result); + } + + return results; + } + } + + private IEnumerable GenerateConverters(Context context) + { + if (!context.IsFrontendClient) + { + yield return FieldConverters.ExcludeHidden(); + yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden()); + } + + yield return FieldConverters.ExcludeChangedTypes(); + yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes()); + + yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig); + yield return FieldConverters.ResolveLanguages(context.App.LanguagesConfig); + + if (!context.IsFrontendClient) + { + yield return FieldConverters.ResolveFallbackLanguages(context.App.LanguagesConfig); + + var languages = context.Languages(); + + if (languages.Any()) + { + yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, languages); + } + + var assetUrls = context.AssetUrls(); + + if (assetUrls.Any()) + { + yield return FieldConverters.ResolveAssetUrls(assetUrls.ToList(), assetUrlGenerator); + } + } + } + + public async Task GetSchemaOrThrowAsync(Context context, string schemaIdOrName) + { + ISchemaEntity? schema = null; + + if (Guid.TryParse(schemaIdOrName, out var id)) + { + schema = await appProvider.GetSchemaAsync(context.App.Id, id); + } + + if (schema == null) + { + schema = await appProvider.GetSchemaAsync(context.App.Id, schemaIdOrName); + } + + if (schema == null) + { + throw new DomainObjectNotFoundException(schemaIdOrName, typeof(ISchemaEntity)); + } + + return schema; + } + + private static void CheckPermission(Context context, params ISchemaEntity[] schemas) + { + foreach (var schema in schemas) + { + if (!HasPermission(context, schema)) + { + throw new DomainForbiddenException("You do not have permission for this schema."); + } + } + } + + private static bool HasPermission(Context context, ISchemaEntity schema) + { + var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name); + + return context.Permissions.Allows(permission); + } + + private static Status[]? GetStatus(Context context) + { + if (context.IsFrontendClient || context.IsUnpublished()) + { + return null; + } + else + { + return StatusPublishedOnly; + } + } + + private async Task> QueryByQueryAsync(Context context, ISchemaEntity schema, Q query) + { + var parsedQuery = queryParser.ParseQuery(context, schema, query); + + return await QueryCoreAsync(context, schema, parsedQuery); + } + + private async Task> QueryByIdsAsync(Context context, ISchemaEntity schema, Q query) + { + var contents = await QueryCoreAsync(context, schema, query.Ids.ToHashSet()); + + return contents.SortSet(x => x.Id, query.Ids); + } + + private Task> QueryCoreAsync(Context context, IReadOnlyList ids) + { + return contentRepository.QueryAsync(context.App, GetStatus(context), new HashSet(ids), WithDraft(context)); + } + + private Task> QueryCoreAsync(Context context, ISchemaEntity schema, ClrQuery query) + { + return contentRepository.QueryAsync(context.App, schema, GetStatus(context), context.IsFrontendClient, query, WithDraft(context)); + } + + private Task> QueryCoreAsync(Context context, ISchemaEntity schema, HashSet ids) + { + return contentRepository.QueryAsync(context.App, schema, GetStatus(context), ids, WithDraft(context)); + } + + private Task FindCoreAsync(Context context, Guid id, ISchemaEntity schema) + { + return contentRepository.FindContentAsync(context.App, schema, GetStatus(context), id, WithDraft(context)); + } + + private Task FindByVersionAsync(Guid id, long version) + { + return contentVersionLoader.GetAsync(id, version); + } + + private static bool WithDraft(Context context) + { + return context.IsUnpublished() || context.IsFrontendClient; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs new file mode 100644 index 000000000..f5cd77bf4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs @@ -0,0 +1,71 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public sealed class FilterTagTransformer : TransformVisitor + { + private readonly ITagService tagService; + private readonly ISchemaEntity schema; + private readonly Guid appId; + + private FilterTagTransformer(Guid appId, ISchemaEntity schema, ITagService tagService) + { + this.appId = appId; + this.schema = schema; + this.tagService = tagService; + } + + public static FilterNode? Transform(FilterNode nodeIn, Guid appId, ISchemaEntity schema, ITagService tagService) + { + Guard.NotNull(nodeIn); + Guard.NotNull(tagService); + Guard.NotNull(schema); + + return nodeIn.Accept(new FilterTagTransformer(appId, schema, tagService)); + } + + public override FilterNode? Visit(CompareFilter nodeIn) + { + if (nodeIn.Value.Value is string stringValue && IsDataPath(nodeIn.Path) && IsTagField(nodeIn.Path)) + { + var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Schemas(schema.Id), HashSet.Of(stringValue))).Result; + + if (tagNames.TryGetValue(stringValue, out var normalized)) + { + return new CompareFilter(nodeIn.Path, nodeIn.Operator, normalized); + } + } + + return nodeIn; + } + + private static bool IsDataPath(IReadOnlyList path) + { + return path.Count == 3 && string.Equals(path[0], nameof(IContentEntity.Data), StringComparison.OrdinalIgnoreCase); + } + + private bool IsTagField(IReadOnlyList path) + { + return schema.SchemaDef.FieldsByName.TryGetValue(path[1], out var field) && IsTagField(field); + } + + private bool IsTagField(IField field) + { + return field is IField tags && tags.Properties.Normalization == TagsFieldNormalization.Schema; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs new file mode 100644 index 000000000..b452e4667 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs @@ -0,0 +1,133 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class QueryExecutionContext + { + private readonly ConcurrentDictionary cachedContents = new ConcurrentDictionary(); + private readonly ConcurrentDictionary cachedAssets = new ConcurrentDictionary(); + private readonly IContentQueryService contentQuery; + private readonly IAssetQueryService assetQuery; + private readonly Context context; + + public QueryExecutionContext(Context context, IAssetQueryService assetQuery, IContentQueryService contentQuery) + { + Guard.NotNull(assetQuery); + Guard.NotNull(contentQuery); + Guard.NotNull(context); + + this.assetQuery = assetQuery; + this.contentQuery = contentQuery; + this.context = context; + } + + public virtual async Task FindAssetAsync(Guid id) + { + var asset = cachedAssets.GetOrDefault(id); + + if (asset == null) + { + asset = await assetQuery.FindAssetAsync(context, id); + + if (asset != null) + { + cachedAssets[asset.Id] = asset; + } + } + + return asset; + } + + public virtual async Task FindContentAsync(Guid schemaId, Guid id) + { + var content = cachedContents.GetOrDefault(id); + + if (content == null) + { + content = await contentQuery.FindContentAsync(context, schemaId.ToString(), id); + + if (content != null) + { + cachedContents[content.Id] = content; + } + } + + return content; + } + + public virtual async Task> QueryAssetsAsync(string query) + { + var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(query)); + + foreach (var asset in assets) + { + cachedAssets[asset.Id] = asset; + } + + return assets; + } + + public virtual async Task> QueryContentsAsync(string schemaIdOrName, string query) + { + var result = await contentQuery.QueryAsync(context, schemaIdOrName, Q.Empty.WithODataQuery(query)); + + foreach (var content in result) + { + cachedContents[content.Id] = content; + } + + return result; + } + + public virtual async Task> GetReferencedAssetsAsync(ICollection ids) + { + Guard.NotNull(ids); + + var notLoadedAssets = new HashSet(ids.Where(id => !cachedAssets.ContainsKey(id))); + + if (notLoadedAssets.Count > 0) + { + var assets = await assetQuery.QueryAsync(context, Q.Empty.WithIds(notLoadedAssets)); + + foreach (var asset in assets) + { + cachedAssets[asset.Id] = asset; + } + } + + return ids.Select(cachedAssets.GetOrDefault).Where(x => x != null).ToList(); + } + + public virtual async Task> GetReferencedContentsAsync(ICollection ids) + { + Guard.NotNull(ids); + + var notLoadedContents = ids.Where(id => !cachedContents.ContainsKey(id)).ToList(); + + if (notLoadedContents.Count > 0) + { + var result = await contentQuery.QueryAsync(context, notLoadedContents); + + foreach (var content in result) + { + cachedContents[content.Id] = content; + } + } + + return ids.Select(cachedContents.GetOrDefault).Where(x => x != null).ToList(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs new file mode 100644 index 000000000..1cb5655a6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.Contents.Repositories +{ + public interface IContentRepository + { + Task> QueryAsync(IAppEntity app, Status[]? status, HashSet ids, bool includeDraft); + + Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, HashSet ids, bool includeDraft); + + Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, bool inDraft, ClrQuery query, bool includeDraft); + + Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode); + + Task> QueryIdsAsync(Guid appId, HashSet ids); + + Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, Guid id, bool includeDraft); + + Task QueryScheduledWithoutDataAsync(Instant now, Func callback); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs new file mode 100644 index 000000000..8e13695c5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -0,0 +1,146 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.Serialization; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +#pragma warning disable IDE0060 // Remove unused parameter + +namespace Squidex.Domain.Apps.Entities.Contents.State +{ + public class ContentState : DomainObjectState, IContentEntity + { + [DataMember] + public NamedId AppId { get; set; } + + [DataMember] + public NamedId SchemaId { get; set; } + + [DataMember] + public NamedContentData Data { get; set; } + + [DataMember] + public NamedContentData DataDraft { get; set; } + + [DataMember] + public ScheduleJob? ScheduleJob { get; set; } + + [DataMember] + public bool IsPending { get; set; } + + [DataMember] + public bool IsDeleted { get; set; } + + [DataMember] + public Status Status { get; set; } + + public void ApplyEvent(IEvent @event) + { + switch (@event) + { + case ContentCreated e: + { + SimpleMapper.Map(e, this); + + UpdateData(null, e.Data, false); + + break; + } + + case ContentChangesPublished _: + { + ScheduleJob = null; + + UpdateData(DataDraft, null, false); + + break; + } + + case ContentStatusChanged e: + { + ScheduleJob = null; + + SimpleMapper.Map(e, this); + + if (e.Status == Status.Published) + { + UpdateData(DataDraft, null, false); + } + + break; + } + + case ContentUpdated e: + { + UpdateData(e.Data, e.Data, false); + + break; + } + + case ContentUpdateProposed e: + { + UpdateData(null, e.Data, true); + + break; + } + + case ContentChangesDiscarded _: + { + UpdateData(null, Data, false); + + break; + } + + case ContentSchedulingCancelled _: + { + ScheduleJob = null; + + break; + } + + case ContentStatusScheduled e: + { + ScheduleJob = ScheduleJob.Build(e.Status, e.Actor, e.DueTime); + + break; + } + + case ContentDeleted _: + { + IsDeleted = true; + + break; + } + } + } + + public override ContentState Apply(Envelope @event) + { + return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); + } + + private void UpdateData(NamedContentData? data, NamedContentData? dataDraft, bool isPending) + { + if (data != null) + { + Data = data; + } + + if (dataDraft != null) + { + DataDraft = dataDraft; + } + + IsPending = isPending; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs new file mode 100644 index 000000000..dc2263426 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs @@ -0,0 +1,117 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public sealed class GrainTextIndexer : ITextIndexer, IEventConsumer + { + private readonly IGrainFactory grainFactory; + + public string Name + { + get { return "TextIndexer"; } + } + + public string EventsFilter + { + get { return "^content-"; } + } + + public GrainTextIndexer(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public bool Handles(StoredEvent @event) + { + return true; + } + + public Task ClearAsync() + { + return TaskHelper.Done; + } + + public async Task On(Envelope @event) + { + if (@event.Payload is ContentEvent contentEvent) + { + var index = grainFactory.GetGrain(contentEvent.SchemaId.Id); + + var id = contentEvent.ContentId; + + switch (@event.Payload) + { + case ContentDeleted _: + await index.DeleteAsync(id); + break; + case ContentCreated contentCreated: + await index.IndexAsync(Data(id, contentCreated.Data, true)); + break; + case ContentUpdateProposed contentUpdateProposed: + await index.IndexAsync(Data(id, contentUpdateProposed.Data, true)); + break; + case ContentUpdated contentUpdated: + await index.IndexAsync(Data(id, contentUpdated.Data, false)); + break; + case ContentChangesDiscarded _: + await index.CopyAsync(id, false); + break; + case ContentChangesPublished _: + case ContentStatusChanged contentStatusChanged when contentStatusChanged.Status == Status.Published: + await index.CopyAsync(id, true); + break; + } + } + } + + private static J Data(Guid contentId, NamedContentData data, bool onlySelf) + { + return new Update { Id = contentId, Data = data, OnlyDraft = onlySelf }; + } + + public async Task?> SearchAsync(string? queryText, IAppEntity app, Guid schemaId, Scope scope = Scope.Published) + { + if (string.IsNullOrWhiteSpace(queryText)) + { + return null; + } + + var index = grainFactory.GetGrain(schemaId); + + using (Profiler.TraceMethod()) + { + var context = CreateContext(app, scope); + + return await index.SearchAsync(queryText, context); + } + } + + private static SearchContext CreateContext(IAppEntity app, Scope scope) + { + var languages = new HashSet(app.LanguagesConfig.Select(x => x.Key)); + + return new SearchContext { Languages = languages, Scope = scope }; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs new file mode 100644 index 000000000..773dd1527 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public interface ITextIndexer + { + Task?> SearchAsync(string? queryText, IAppEntity app, Guid schemaId, Scope scope = Scope.Published); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexState.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexState.cs new file mode 100644 index 000000000..18d3956d4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexState.cs @@ -0,0 +1,144 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Lucene.Net.Documents; +using Lucene.Net.Index; +using Lucene.Net.Search; +using Lucene.Net.Util; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + internal sealed class IndexState + { + private const int NotFound = -1; + private const string MetaFor = "_fd"; + private readonly IndexSearcher? indexSearcher; + private readonly IndexWriter indexWriter; + private readonly BinaryDocValues binaryValues; + private readonly Dictionary<(Guid, byte), BytesRef> changes = new Dictionary<(Guid, byte), BytesRef>(); + private bool isClosed; + + public IndexState(IndexWriter indexWriter, IndexReader? indexReader = null, IndexSearcher? indexSearcher = null) + { + this.indexSearcher = indexSearcher; + this.indexWriter = indexWriter; + + if (indexReader != null) + { + binaryValues = MultiDocValues.GetBinaryValues(indexReader, MetaFor); + } + } + + public void Index(Guid id, byte draft, Document document, byte forDraft, byte forPublished) + { + var value = GetValue(forDraft, forPublished); + + document.SetBinaryDocValue(MetaFor, value); + + changes[(id, draft)] = value; + } + + public void Index(Guid id, byte draft, Term term, byte forDraft, byte forPublished) + { + var value = GetValue(forDraft, forPublished); + + indexWriter.UpdateBinaryDocValue(term, MetaFor, value); + + changes[(id, draft)] = value; + } + + public bool HasBeenAdded(Guid id, byte draft, Term term, out int docId) + { + docId = 0; + + if (changes.ContainsKey((id, draft))) + { + return true; + } + + if (indexSearcher != null && !isClosed) + { + var docs = indexSearcher.Search(new TermQuery(term), 1); + + docId = docs?.ScoreDocs.FirstOrDefault()?.Doc ?? NotFound; + + return docId > NotFound; + } + + return false; + } + + public bool TryGet(Guid id, byte draft, int docId, out byte forDraft, out byte forPublished) + { + forDraft = 0; + forPublished = 0; + + if (changes.TryGetValue((id, draft), out var forValue)) + { + forDraft = forValue.Bytes[0]; + forPublished = forValue.Bytes[1]; + + return true; + } + + if (!isClosed && docId != NotFound) + { + forValue = new BytesRef(); + + binaryValues?.Get(docId, forValue); + + if (forValue.Bytes.Length == 2) + { + forDraft = forValue.Bytes[0]; + forPublished = forValue.Bytes[1]; + + changes[(id, draft)] = forValue; + + return true; + } + } + + return false; + } + + public bool TryGet(int docId, out byte forDraft, out byte forPublished) + { + forDraft = 0; + forPublished = 0; + + if (!isClosed && docId != NotFound) + { + var forValue = new BytesRef(); + + binaryValues?.Get(docId, forValue); + + if (forValue.Bytes.Length == 2) + { + forDraft = forValue.Bytes[0]; + forPublished = forValue.Bytes[1]; + + return true; + } + } + + return false; + } + + private static BytesRef GetValue(byte forDraft, byte forPublished) + { + return new BytesRef(new[] { forDraft, forPublished }); + } + + public void CloseReader() + { + isClosed = true; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs new file mode 100644 index 000000000..80a98fbb9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Lucene.Net.Analysis; +using Lucene.Net.Analysis.Standard; +using Lucene.Net.Util; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public sealed class MultiLanguageAnalyzer : AnalyzerWrapper + { + private readonly StandardAnalyzer fallbackAnalyzer; + private readonly Dictionary analyzers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public MultiLanguageAnalyzer(LuceneVersion version) + : base(PER_FIELD_REUSE_STRATEGY) + { + fallbackAnalyzer = new StandardAnalyzer(version); + + foreach (var type in typeof(StandardAnalyzer).Assembly.GetTypes()) + { + if (typeof(Analyzer).IsAssignableFrom(type)) + { + var language = type.Namespace!.Split('.').Last(); + + if (language.Length == 2) + { + try + { + var analyzer = Activator.CreateInstance(type, version)!; + + analyzers[language] = (Analyzer)analyzer; + } + catch (MissingMethodException) + { + continue; + } + } + } + } + } + + protected override Analyzer GetWrappedAnalyzer(string fieldName) + { + if (fieldName.Length > 0) + { + var analyzer = analyzers.GetOrDefault(fieldName.Substring(0, 2)) ?? fallbackAnalyzer; + + return analyzer; + } + else + { + return fallbackAnalyzer; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/Scope.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Scope.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Text/Scope.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Scope.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexContent.cs new file mode 100644 index 000000000..83f701b7c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexContent.cs @@ -0,0 +1,213 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Text; +using Lucene.Net.Documents; +using Lucene.Net.Index; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + internal sealed class TextIndexContent + { + private const string MetaId = "_id"; + private const string MetaKey = "_key"; + private readonly IndexWriter indexWriter; + private readonly IndexState indexState; + private readonly Guid id; + + public TextIndexContent(IndexWriter indexWriter, IndexState indexState, Guid id) + { + this.indexWriter = indexWriter; + this.indexState = indexState; + + this.id = id; + } + + public void Delete() + { + indexWriter.DeleteDocuments(new Term(MetaId, id.ToString())); + } + + public static bool TryGetId(int docId, Scope scope, IndexReader reader, IndexState indexState, out Guid result) + { + result = Guid.Empty; + + if (!indexState.TryGet(docId, out var draft, out var published)) + { + return false; + } + + if (scope == Scope.Draft && draft != 1) + { + return false; + } + + if (scope == Scope.Published && published != 1) + { + return false; + } + + var document = reader.Document(docId); + + var idString = document.Get(MetaId); + + if (!Guid.TryParse(idString, out result)) + { + return false; + } + + return true; + } + + public void Index(NamedContentData data, bool onlyDraft) + { + var converted = CreateDocument(data); + + Upsert(converted, 1, 1, 0); + + var isPublishDocumentAdded = IsAdded(0, out var docId); + var isPublishForPublished = IsForPublished(0, docId); + + if (!onlyDraft && isPublishDocumentAdded && isPublishForPublished) + { + Upsert(converted, 0, 0, 1); + } + else if (!onlyDraft || !isPublishDocumentAdded) + { + Upsert(converted, 0, 0, 0); + } + else + { + UpdateFor(0, 0, isPublishForPublished ? (byte)1 : (byte)0); + } + } + + public void Copy(bool fromDraft) + { + if (fromDraft) + { + UpdateFor(1, 1, 0); + UpdateFor(0, 0, 1); + } + else + { + UpdateFor(1, 0, 0); + UpdateFor(0, 1, 1); + } + } + + private static Document CreateDocument(NamedContentData data) + { + var languages = new Dictionary(); + + void AppendText(string language, string text) + { + if (!string.IsNullOrWhiteSpace(text)) + { + var sb = languages.GetOrAddNew(language); + + if (sb.Length > 0) + { + sb.Append(" "); + } + + sb.Append(text); + } + } + + foreach (var field in data) + { + if (field.Value != null) + { + foreach (var fieldValue in field.Value) + { + var appendText = new Action(text => AppendText(fieldValue.Key, text)); + + AppendJsonText(fieldValue.Value, appendText); + } + } + } + + var document = new Document(); + + foreach (var field in languages) + { + document.AddTextField(field.Key, field.Value.ToString(), Field.Store.NO); + } + + return document; + } + + private void UpdateFor(byte draft, byte forDraft, byte forPublished) + { + var term = new Term(MetaKey, BuildKey(draft)); + + indexState.Index(id, draft, term, forDraft, forPublished); + } + + private void Upsert(Document document, byte draft, byte forDraft, byte forPublished) + { + if (document != null) + { + document.RemoveField(MetaId); + document.RemoveField(MetaKey); + + var contentId = id.ToString(); + var contentKey = BuildKey(draft); + + document.AddStringField(MetaId, contentId, Field.Store.YES); + document.AddStringField(MetaKey, contentKey, Field.Store.YES); + + indexState.Index(id, draft, document, forDraft, forPublished); + + indexWriter.UpdateDocument(new Term(MetaKey, contentKey), document); + } + } + + private static void AppendJsonText(IJsonValue value, Action appendText) + { + if (value.Type == JsonValueType.String) + { + appendText(value.ToString()); + } + else if (value is JsonArray array) + { + foreach (var item in array) + { + AppendJsonText(item, appendText); + } + } + else if (value is JsonObject obj) + { + foreach (var item in obj.Values) + { + AppendJsonText(item, appendText); + } + } + } + + private bool IsAdded(byte draft, out int docId) + { + return indexState.HasBeenAdded(id, draft, new Term(MetaKey, BuildKey(draft)), out docId); + } + + private bool IsForPublished(byte draft, int docId) + { + return indexState.TryGet(id, draft, docId, out _, out var p) && p == 1; + } + + private string BuildKey(byte draft) + { + return $"{id}_{draft}"; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs new file mode 100644 index 000000000..6730a6203 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs @@ -0,0 +1,271 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Lucene.Net.Analysis; +using Lucene.Net.Index; +using Lucene.Net.QueryParsers.Classic; +using Lucene.Net.Search; +using Lucene.Net.Store; +using Lucene.Net.Util; +using Squidex.Domain.Apps.Core; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public sealed class TextIndexerGrain : GrainOfGuid, ITextIndexerGrain + { + private const LuceneVersion Version = LuceneVersion.LUCENE_48; + private const int MaxResults = 2000; + private const int MaxUpdates = 400; + private static readonly TimeSpan CommitDelay = TimeSpan.FromSeconds(10); + private static readonly MergeScheduler MergeScheduler = new ConcurrentMergeScheduler(); + private static readonly Analyzer Analyzer = new MultiLanguageAnalyzer(Version); + private static readonly string[] Invariant = { InvariantPartitioning.Key }; + private readonly SnapshotDeletionPolicy snapshotter = new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()); + private readonly IAssetStore assetStore; + private IDisposable? timer; + private DirectoryInfo directory; + private IndexWriter? indexWriter; + private IndexReader? indexReader; + private IndexSearcher? indexSearcher; + private IndexState? indexState; + private QueryParser? queryParser; + private HashSet? currentLanguages; + private int updates; + + public TextIndexerGrain(IAssetStore assetStore) + { + Guard.NotNull(assetStore); + + this.assetStore = assetStore; + } + + public override async Task OnDeactivateAsync() + { + await DeactivateAsync(true); + } + + protected override async Task OnActivateAsync(Guid key) + { + directory = new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"Index_{key}")); + + await assetStore.DownloadAsync(directory); + + var config = new IndexWriterConfig(Version, Analyzer) + { + IndexDeletionPolicy = snapshotter, + MergePolicy = new TieredMergePolicy(), + MergeScheduler = MergeScheduler + }; + + indexWriter = new IndexWriter(FSDirectory.Open(directory), config); + + if (indexWriter.NumDocs > 0) + { + OpenReader(); + } + else + { + indexState = new IndexState(indexWriter); + } + } + + public Task IndexAsync(J update) + { + return IndexInternalAsync(update); + } + + private Task IndexInternalAsync(Update update) + { + if (indexWriter != null && indexState != null) + { + var content = new TextIndexContent(indexWriter, indexState, update.Id); + + content.Index(update.Data, update.OnlyDraft); + } + + return TryFlushAsync(); + } + + public Task CopyAsync(Guid id, bool fromDraft) + { + if (indexWriter != null && indexState != null) + { + var content = new TextIndexContent(indexWriter, indexState, id); + + content.Copy(fromDraft); + } + + return TryFlushAsync(); + } + + public Task DeleteAsync(Guid id) + { + if (indexWriter != null && indexState != null) + { + var content = new TextIndexContent(indexWriter, indexState, id); + + content.Delete(); + } + + return TryFlushAsync(); + } + + public Task> SearchAsync(string queryText, SearchContext context) + { + var result = new List(); + + if (!string.IsNullOrWhiteSpace(queryText)) + { + var query = BuildQuery(queryText, context); + + if (indexReader == null && indexWriter?.NumDocs > 0) + { + OpenReader(); + } + + if (indexReader != null && indexSearcher != null && indexState != null) + { + var found = new HashSet(); + + var hits = indexSearcher.Search(query, MaxResults).ScoreDocs; + + foreach (var hit in hits) + { + if (TextIndexContent.TryGetId(hit.Doc, context.Scope, indexReader, indexState, out var id)) + { + if (found.Add(id)) + { + result.Add(id); + } + } + } + } + } + + return Task.FromResult(result.ToList()); + } + + private Query BuildQuery(string query, SearchContext context) + { + if (queryParser == null || currentLanguages == null || !currentLanguages.SetEquals(context.Languages)) + { + var fields = context.Languages.Union(Invariant).ToArray(); + + queryParser = new MultiFieldQueryParser(Version, fields, Analyzer); + + currentLanguages = context.Languages; + } + + try + { + return queryParser.Parse(query); + } + catch (ParseException ex) + { + throw new ValidationException(ex.Message); + } + } + + private async Task TryFlushAsync() + { + timer?.Dispose(); + + updates++; + + if (updates >= MaxUpdates) + { + await FlushAsync(); + + return true; + } + else + { + CleanReader(); + + try + { + timer = RegisterTimer(_ => FlushAsync(), null, CommitDelay, CommitDelay); + } + catch (InvalidOperationException) + { + return false; + } + } + + return false; + } + + public async Task FlushAsync() + { + if (updates > 0 && indexWriter != null) + { + indexWriter.Commit(); + indexWriter.Flush(true, true); + + CleanReader(); + + var commit = snapshotter.Snapshot(); + try + { + await assetStore.UploadDirectoryAsync(directory, commit); + } + finally + { + snapshotter.Release(commit); + } + + updates = 0; + } + } + + public async Task DeactivateAsync(bool deleteFolder = false) + { + await FlushAsync(); + + CleanWriter(); + CleanReader(); + + if (deleteFolder && directory.Exists) + { + directory.Delete(true); + } + } + + private void OpenReader() + { + if (indexWriter != null) + { + indexReader = indexWriter!.GetReader(true); + indexSearcher = new IndexSearcher(indexReader); + indexState = new IndexState(indexWriter, indexReader, indexSearcher); + } + } + + private void CleanReader() + { + indexReader?.Dispose(); + indexReader = null; + indexSearcher = null; + indexState?.CloseReader(); + } + + private void CleanWriter() + { + indexWriter?.Dispose(); + indexWriter = null; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/Update.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Update.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Text/Update.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Update.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Context.cs b/backend/src/Squidex.Domain.Apps.Entities/Context.cs new file mode 100644 index 000000000..903868e14 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Context.cs @@ -0,0 +1,71 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Shared.Identity; +using ClaimsPermissions = Squidex.Infrastructure.Security.PermissionSet; + +namespace Squidex.Domain.Apps.Entities +{ + public sealed class Context + { + public IDictionary Headers { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public IAppEntity App { get; set; } + + public ClaimsPrincipal User { get; } + + public ClaimsPermissions Permissions { get; private set; } = ClaimsPermissions.Empty; + + public bool IsFrontendClient { get; private set; } + + public Context(ClaimsPrincipal user) + { + Guard.NotNull(user); + + User = user; + + UpdatePermissions(); + } + + public Context(ClaimsPrincipal user, IAppEntity app) + : this(user) + { + App = app; + } + + public static Context Anonymous() + { + return new Context(new ClaimsPrincipal()); + } + + public void UpdatePermissions() + { + Permissions = User.Permissions(); + + IsFrontendClient = User.IsInClient(DefaultClients.Frontend); + } + + public Context Clone() + { + var clone = new Context(User, App); + + foreach (var kvp in Headers) + { + clone.Headers[kvp.Key] = kvp.Value; + } + + return clone; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/DomainObjectState.cs rename to backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs diff --git a/src/Squidex.Domain.Apps.Entities/EntityExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/EntityExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/EntityExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/EntityExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs b/backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs new file mode 100644 index 000000000..9e13f4f36 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs @@ -0,0 +1,82 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities +{ + public static class EntityMapper + { + public static T Update(this T entity, Envelope envelope, Action? updater = null) where T : IEntity + { + var @event = (SquidexEvent)envelope.Payload; + + var headers = envelope.Headers; + + SetId(entity, headers); + SetCreated(entity, headers); + SetCreatedBy(entity, @event); + SetLastModified(entity, headers); + SetLastModifiedBy(entity, @event); + SetVersion(entity, headers); + + updater?.Invoke(@event, entity); + + return entity; + } + + private static void SetId(IEntity entity, EnvelopeHeaders headers) + { + if (entity is IUpdateableEntity updateable && updateable.Id == Guid.Empty) + { + updateable.Id = headers.AggregateId(); + } + } + + private static void SetVersion(IEntity entity, EnvelopeHeaders headers) + { + if (entity is IUpdateableEntityWithVersion updateable) + { + updateable.Version = headers.EventStreamNumber(); + } + } + + private static void SetCreated(IEntity entity, EnvelopeHeaders headers) + { + if (entity is IUpdateableEntity updateable && updateable.Created == default) + { + updateable.Created = headers.Timestamp(); + } + } + + private static void SetCreatedBy(IEntity entity, SquidexEvent @event) + { + if (entity is IUpdateableEntityWithCreatedBy withCreatedBy && withCreatedBy.CreatedBy == null) + { + withCreatedBy.CreatedBy = @event.Actor; + } + } + + private static void SetLastModified(IEntity entity, EnvelopeHeaders headers) + { + if (entity is IUpdateableEntity updateable) + { + updateable.LastModified = headers.Timestamp(); + } + } + + private static void SetLastModifiedBy(IEntity entity, SquidexEvent @event) + { + if (entity is IUpdateableEntityWithLastModifiedBy withModifiedBy) + { + withModifiedBy.LastModifiedBy = @event.Actor; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs new file mode 100644 index 000000000..a354c4a2b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.History +{ + public sealed class HistoryEvent + { + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid AppId { get; set; } + + public Instant Created { get; set; } + + public RefToken Actor { get; set; } + + public long Version { get; set; } + + public string Channel { get; set; } + + public string Message { get; set; } + + public Dictionary Parameters { get; set; } = new Dictionary(); + + public HistoryEvent() + { + } + + public HistoryEvent(string channel, string message) + { + Guard.NotNullOrEmpty(channel); + Guard.NotNullOrEmpty(message); + + Channel = channel; + + Message = message; + } + + public HistoryEvent Param(string key, object? value) + { + if (value != null) + { + var formatted = value.ToString(); + + if (formatted != null) + { + Parameters[key] = formatted; + } + } + + return this; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs new file mode 100644 index 000000000..5b85a97fe --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.History +{ + public abstract class HistoryEventsCreatorBase : IHistoryEventsCreator + { + private readonly Dictionary texts = new Dictionary(); + private readonly TypeNameRegistry typeNameRegistry; + + public IReadOnlyDictionary Texts + { + get { return texts; } + } + + protected HistoryEventsCreatorBase(TypeNameRegistry typeNameRegistry) + { + Guard.NotNull(typeNameRegistry); + + this.typeNameRegistry = typeNameRegistry; + } + + protected void AddEventMessage(string message) where TEvent : IEvent + { + Guard.NotNullOrEmpty(message); + + texts[typeNameRegistry.GetName()] = message; + } + + protected bool HasEventText(IEvent @event) + { + var message = typeNameRegistry.GetName(@event.GetType()); + + return texts.ContainsKey(message); + } + + protected HistoryEvent ForEvent(IEvent @event, string channel) + { + var message = typeNameRegistry.GetName(@event.GetType()); + + return new HistoryEvent(channel, message); + } + + public Task CreateEventAsync(Envelope @event) + { + if (HasEventText(@event.Payload)) + { + return CreateEventCoreAsync(@event); + } + + return Task.FromResult(null); + } + + protected abstract Task CreateEventCoreAsync(Envelope @event); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs new file mode 100644 index 000000000..426f8d450 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs @@ -0,0 +1,90 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.History.Repositories; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.History +{ + public sealed class HistoryService : IHistoryService, IEventConsumer + { + private readonly Dictionary texts = new Dictionary(); + private readonly List creators; + private readonly IHistoryEventRepository repository; + + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return ".*"; } + } + + public HistoryService(IHistoryEventRepository repository, IEnumerable creators) + { + Guard.NotNull(repository); + Guard.NotNull(creators); + + this.creators = creators.ToList(); + + foreach (var creator in this.creators) + { + foreach (var text in creator.Texts) + { + texts[text.Key] = text.Value; + } + } + + this.repository = repository; + } + + public bool Handles(StoredEvent @event) + { + return true; + } + + public Task ClearAsync() + { + return repository.ClearAsync(); + } + + public async Task On(Envelope @event) + { + foreach (var creator in creators) + { + var historyEvent = await creator.CreateEventAsync(@event); + + if (historyEvent != null) + { + var appEvent = (AppEvent)@event.Payload; + + historyEvent.Actor = appEvent.Actor; + historyEvent.AppId = appEvent.AppId.Id; + historyEvent.Created = @event.Headers.Timestamp(); + historyEvent.Version = @event.Headers.EventStreamNumber(); + + await repository.InsertAsync(historyEvent); + } + } + } + + public async Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count) + { + var items = await repository.QueryByChannelAsync(appId, channelPrefix, count); + + return items.Select(x => new ParsedHistoryEvent(x, texts)).ToList(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs new file mode 100644 index 000000000..6e7a097f1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.History +{ + public interface IHistoryEventsCreator + { + IReadOnlyDictionary Texts { get; } + + Task CreateEventAsync(Envelope @event); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs rename to backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs diff --git a/src/Squidex.Domain.Apps.Entities/History/Notifications/INotificationEmailSender.cs b/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/INotificationEmailSender.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/History/Notifications/INotificationEmailSender.cs rename to backend/src/Squidex.Domain.Apps.Entities/History/Notifications/INotificationEmailSender.cs diff --git a/src/Squidex.Domain.Apps.Entities/History/Notifications/NoopNotificationEmailSender.cs b/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NoopNotificationEmailSender.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/History/Notifications/NoopNotificationEmailSender.cs rename to backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NoopNotificationEmailSender.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailEventConsumer.cs b/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailEventConsumer.cs new file mode 100644 index 000000000..a521530a6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailEventConsumer.cs @@ -0,0 +1,121 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.History.Notifications +{ + public sealed class NotificationEmailEventConsumer : IEventConsumer + { + private static readonly Duration MaxAge = Duration.FromDays(2); + private readonly INotificationEmailSender emailSender; + private readonly IUserResolver userResolver; + private readonly ISemanticLog log; + + public string Name + { + get { return "NotificationEmailSender"; } + } + + public string EventsFilter + { + get { return "^app-"; } + } + + public NotificationEmailEventConsumer(INotificationEmailSender emailSender, IUserResolver userResolver, ISemanticLog log) + { + Guard.NotNull(emailSender); + Guard.NotNull(userResolver); + Guard.NotNull(log); + + this.emailSender = emailSender; + this.userResolver = userResolver; + + this.log = log; + } + + public bool Handles(StoredEvent @event) + { + return true; + } + + public Task ClearAsync() + { + return TaskHelper.Done; + } + + public async Task On(Envelope @event) + { + if (!emailSender.IsActive) + { + return; + } + + if (@event.Headers.EventStreamNumber() <= 1) + { + return; + } + + var now = SystemClock.Instance.GetCurrentInstant(); + + var timestamp = @event.Headers.Timestamp(); + + if (now - timestamp > MaxAge) + { + return; + } + + if (@event.Payload is AppContributorAssigned appContributorAssigned) + { + if (!appContributorAssigned.Actor.IsSubject || !appContributorAssigned.IsAdded) + { + return; + } + + var assignerId = appContributorAssigned.Actor.Identifier; + var assigneeId = appContributorAssigned.ContributorId; + + var assigner = await userResolver.FindByIdOrEmailAsync(assignerId); + + if (assigner == null) + { + LogWarning($"Assigner {assignerId} not found"); + return; + } + + var assignee = await userResolver.FindByIdOrEmailAsync(appContributorAssigned.ContributorId); + + if (assignee == null) + { + LogWarning($"Assignee {assigneeId} not found"); + return; + } + + var appName = appContributorAssigned.AppId.Name; + + var isCreated = appContributorAssigned.IsCreated; + + await emailSender.SendContributorEmailAsync(assigner, assignee, appName, isCreated); + } + } + + private void LogWarning(string reason) + { + log.LogWarning(w => w + .WriteProperty("action", "InviteUser") + .WriteProperty("status", "Failed") + .WriteProperty("reason", reason)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailSender.cs b/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailSender.cs new file mode 100644 index 000000000..f76c6087a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailSender.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Email; +using Squidex.Infrastructure.Log; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.History.Notifications +{ + public sealed class NotificationEmailSender : INotificationEmailSender + { + private readonly IEmailSender emailSender; + private readonly IEmailUrlGenerator emailUrlGenerator; + private readonly ISemanticLog log; + private readonly NotificationEmailTextOptions texts; + + public bool IsActive + { + get { return true; } + } + + public NotificationEmailSender( + IOptions texts, + IEmailSender emailSender, + IEmailUrlGenerator emailUrlGenerator, + ISemanticLog log) + { + Guard.NotNull(texts); + Guard.NotNull(emailSender); + Guard.NotNull(emailUrlGenerator); + Guard.NotNull(log); + + this.texts = texts.Value; + this.emailSender = emailSender; + this.emailUrlGenerator = emailUrlGenerator; + this.log = log; + } + + public Task SendContributorEmailAsync(IUser assigner, IUser assignee, string appName, bool isCreated) + { + Guard.NotNull(assigner); + Guard.NotNull(assignee); + Guard.NotNull(appName); + + if (assignee.HasConsent()) + { + return SendEmailAsync(texts.ExistingUserSubject, texts.ExistingUserBody, assigner, assignee, appName); + } + else + { + return SendEmailAsync(texts.NewUserSubject, texts.NewUserBody, assigner, assignee, appName); + } + } + + private async Task SendEmailAsync(string emailSubj, string emailBody, IUser assigner, IUser assignee, string appName) + { + if (string.IsNullOrWhiteSpace(emailBody)) + { + LogWarning("No email subject configured for new users"); + return; + } + + if (string.IsNullOrWhiteSpace(emailSubj)) + { + LogWarning("No email body configured for new users"); + return; + } + + var appUrl = emailUrlGenerator.GenerateUIUrl(); + + emailSubj = Format(emailSubj, assigner, assignee, appUrl, appName); + emailBody = Format(emailBody, assigner, assignee, appUrl, appName); + + await emailSender.SendAsync(assignee.Email, emailSubj, emailBody); + } + + private void LogWarning(string reason) + { + log.LogWarning(w => w + .WriteProperty("action", "InviteUser") + .WriteProperty("status", "Failed") + .WriteProperty("reason", reason)); + } + + private static string Format(string text, IUser assigner, IUser assignee, string uiUrl, string appName) + { + text = text.Replace("$APP_NAME", appName); + + if (assigner != null) + { + text = text.Replace("$ASSIGNER_EMAIL", assigner.Email); + text = text.Replace("$ASSIGNER_NAME", assigner.DisplayName()); + } + + if (assignee != null) + { + text = text.Replace("$ASSIGNEE_EMAIL", assignee.Email); + text = text.Replace("$ASSIGNEE_NAME", assignee.DisplayName()); + } + + text = text.Replace("$UI_URL", uiUrl); + + return text; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailTextOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailTextOptions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailTextOptions.cs rename to backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailTextOptions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs new file mode 100644 index 000000000..0d7b503c2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs @@ -0,0 +1,70 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.History +{ + public sealed class ParsedHistoryEvent + { + private readonly HistoryEvent item; + private readonly Lazy message; + + public Guid Id + { + get { return item.Id; } + } + + public Instant Created + { + get { return item.Created; } + } + + public RefToken Actor + { + get { return item.Actor; } + } + + public long Version + { + get { return item.Version; } + } + + public string Channel + { + get { return item.Channel; } + } + + public string? Message + { + get { return message.Value; } + } + + public ParsedHistoryEvent(HistoryEvent item, IReadOnlyDictionary texts) + { + this.item = item; + + message = new Lazy(() => + { + if (texts.TryGetValue(item.Message, out var result)) + { + foreach (var kvp in item.Parameters) + { + result = result.Replace("[" + kvp.Key + "]", kvp.Value); + } + + return result; + } + + return null; + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs rename to backend/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs diff --git a/src/Squidex.Domain.Apps.Entities/IAppCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/IAppCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IAppCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/IAppCommand.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs new file mode 100644 index 000000000..bc4dd03ff --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure.Security; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IAppProvider + { + Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaAsync(Guid appId, Guid id); + + Task GetAppAsync(Guid appId); + + Task GetAppAsync(string appName); + + Task> GetUserAppsAsync(string userId, PermissionSet permissions); + + Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false); + + Task GetSchemaAsync(Guid appId, string name); + + Task> GetSchemasAsync(Guid appId); + + Task> GetRulesAsync(Guid appId); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/IContextProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/IContextProvider.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IContextProvider.cs rename to backend/src/Squidex.Domain.Apps.Entities/IContextProvider.cs diff --git a/src/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs b/backend/src/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs rename to backend/src/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs diff --git a/src/Squidex.Domain.Apps.Entities/IEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/IEntity.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs new file mode 100644 index 000000000..516aead56 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IEntityWithCacheDependencies + { + HashSet CacheDependencies { get; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs rename to backend/src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs rename to backend/src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs rename to backend/src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs rename to backend/src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs diff --git a/src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs b/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs rename to backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs b/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs rename to backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs b/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs rename to backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Q.cs b/backend/src/Squidex.Domain.Apps.Entities/Q.cs new file mode 100644 index 000000000..863e14796 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Q.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities +{ + public sealed class Q : Cloneable + { + public static readonly Q Empty = new Q(); + + public IReadOnlyList Ids { get; private set; } + + public string? ODataQuery { get; private set; } + + public string? JsonQuery { get; private set; } + + public Q WithODataQuery(string? odataQuery) + { + return Clone(c => c.ODataQuery = odataQuery); + } + + public Q WithJsonQuery(string? jsonQuery) + { + return Clone(c => c.JsonQuery = jsonQuery); + } + + public Q WithIds(params Guid[] ids) + { + return Clone(c => c.Ids = ids.ToList()); + } + + public Q WithIds(IEnumerable ids) + { + return Clone(c => c.Ids = ids.ToList()); + } + + public Q WithIds(string? ids) + { + if (!string.IsNullOrEmpty(ids)) + { + return Clone(c => + { + var idsList = new List(); + + foreach (var id in ids.Split(',')) + { + if (Guid.TryParse(id, out var guid)) + { + idsList.Add(guid); + } + } + + c.Ids = idsList; + }); + } + + return this; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs new file mode 100644 index 000000000..01226e61d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Domain.Apps.Entities.Rules.Indexes; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public sealed class BackupRules : BackupHandler + { + private readonly HashSet ruleIds = new HashSet(); + private readonly IRulesIndex indexForRules; + + public override string Name { get; } = "Rules"; + + public BackupRules(IRulesIndex indexForRules) + { + Guard.NotNull(indexForRules); + + this.indexForRules = indexForRules; + } + + public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) + { + switch (@event.Payload) + { + case RuleCreated ruleCreated: + ruleIds.Add(ruleCreated.RuleId); + break; + case RuleDeleted ruleDeleted: + ruleIds.Remove(ruleDeleted.RuleId); + break; + } + + return TaskHelper.True; + } + + public override Task RestoreAsync(Guid appId, BackupReader reader) + { + return indexForRules.RebuildAsync(appId, ruleIds); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs new file mode 100644 index 000000000..2f8a80964 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs @@ -0,0 +1,106 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Rules.Guards +{ + public static class GuardRule + { + public static Task CanCreate(CreateRule command, IAppProvider appProvider) + { + Guard.NotNull(command); + + return Validate.It(() => "Cannot create rule.", async e => + { + if (command.Trigger == null) + { + e(Not.Defined("Trigger"), nameof(command.Trigger)); + } + else + { + var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Id, command.Trigger, appProvider); + + errors.Foreach(x => x.AddTo(e)); + } + + if (command.Action == null) + { + e(Not.Defined("Action"), nameof(command.Action)); + } + else + { + var errors = command.Action.Validate(); + + errors.Foreach(x => x.AddTo(e)); + } + }); + } + + public static Task CanUpdate(UpdateRule command, Guid appId, IAppProvider appProvider, Rule rule) + { + Guard.NotNull(command); + + return Validate.It(() => "Cannot update rule.", async e => + { + if (command.Trigger == null && command.Action == null && command.Name == null) + { + e(Not.Defined("Either trigger, action or name"), nameof(command.Trigger), nameof(command.Action)); + } + + if (command.Trigger != null) + { + var errors = await RuleTriggerValidator.ValidateAsync(appId, command.Trigger, appProvider); + + errors.Foreach(x => x.AddTo(e)); + } + + if (command.Action != null) + { + var errors = command.Action.Validate(); + + errors.Foreach(x => x.AddTo(e)); + } + + if (command.Name != null && string.Equals(rule.Name, command.Name)) + { + e(Not.New("Rule", "name"), nameof(command.Name)); + } + }); + } + + public static void CanEnable(EnableRule command, Rule rule) + { + Guard.NotNull(command); + + if (rule.IsEnabled) + { + throw new DomainException("Rule is already enabled."); + } + } + + public static void CanDisable(DisableRule command, Rule rule) + { + Guard.NotNull(command); + + if (!rule.IsEnabled) + { + throw new DomainException("Rule is already disabled."); + } + } + + public static void CanDelete(DeleteRule command) + { + Guard.NotNull(command); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs new file mode 100644 index 000000000..64c4a1301 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Rules.Guards +{ + public sealed class RuleTriggerValidator : IRuleTriggerVisitor>> + { + public Func> SchemaProvider { get; } + + public RuleTriggerValidator(Func> schemaProvider) + { + SchemaProvider = schemaProvider; + } + + public static Task> ValidateAsync(Guid appId, RuleTrigger action, IAppProvider appProvider) + { + Guard.NotNull(action); + Guard.NotNull(appProvider); + + var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appId, x)); + + return action.Accept(visitor); + } + + public Task> Visit(AssetChangedTriggerV2 trigger) + { + return Task.FromResult(Enumerable.Empty()); + } + + public Task> Visit(ManualTrigger trigger) + { + return Task.FromResult(Enumerable.Empty()); + } + + public Task> Visit(SchemaChangedTrigger trigger) + { + return Task.FromResult(Enumerable.Empty()); + } + + public Task> Visit(UsageTrigger trigger) + { + var errors = new List(); + + if (trigger.NumDays.HasValue && (trigger.NumDays < 1 || trigger.NumDays > 30)) + { + errors.Add(new ValidationError(Not.Between("Num days", 1, 30), nameof(trigger.NumDays))); + } + + return Task.FromResult>(errors); + } + + public async Task> Visit(ContentChangedTriggerV2 trigger) + { + var errors = new List(); + + if (trigger.Schemas != null) + { + var tasks = new List>(); + + foreach (var schema in trigger.Schemas) + { + if (schema.SchemaId == Guid.Empty) + { + errors.Add(new ValidationError(Not.Defined("Schema id"), nameof(trigger.Schemas))); + } + else + { + tasks.Add(CheckSchemaAsync(schema)); + } + } + + var checkErrors = await Task.WhenAll(tasks); + + errors.AddRange(checkErrors.Where(x => x != null)); + } + + return errors; + } + + private async Task CheckSchemaAsync(ContentChangedTriggerSchemaV2 schema) + { + if (await SchemaProvider(schema.SchemaId) == null) + { + return new ValidationError($"Schema {schema.SchemaId} does not exist.", nameof(ContentChangedTriggerV2.Schemas)); + } + + return null; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs new file mode 100644 index 000000000..c45443510 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public interface IRuleEventEntity : IEntity + { + RuleJob Job { get; } + + Instant? NextAttempt { get; } + + RuleJobResult JobResult { get; } + + RuleResult Result { get; } + + int NumCalls { get; } + + string? LastDump { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/IRuleGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs new file mode 100644 index 000000000..c3a346594 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Rules.Indexes +{ + public sealed class RulesIndex : ICommandMiddleware, IRulesIndex + { + private readonly IGrainFactory grainFactory; + + public RulesIndex(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public Task RebuildAsync(Guid appId, HashSet rues) + { + return Index(appId).RebuildAsync(rues); + } + + public async Task> GetRulesAsync(Guid appId) + { + using (Profiler.TraceMethod()) + { + var ids = await GetRuleIdsAsync(appId); + + var rules = + await Task.WhenAll( + ids.Select(GetRuleAsync)); + + return rules.Where(x => x != null).ToList(); + } + } + + private async Task GetRuleAsync(Guid id) + { + using (Profiler.TraceMethod()) + { + var ruleEntity = await grainFactory.GetGrain(id).GetStateAsync(); + + if (IsFound(ruleEntity.Value)) + { + return ruleEntity.Value; + } + + return null; + } + } + + private async Task> GetRuleIdsAsync(Guid appId) + { + using (Profiler.TraceMethod()) + { + return await Index(appId).GetIdsAsync(); + } + } + + public async Task HandleAsync(CommandContext context, Func next) + { + await next(); + + if (context.IsCompleted) + { + switch (context.Command) + { + case CreateRule createRule: + await CreateRuleAsync(createRule); + break; + case DeleteRule deleteRule: + await DeleteRuleAsync(deleteRule); + break; + } + } + } + + private async Task CreateRuleAsync(CreateRule command) + { + await Index(command.AppId.Id).AddAsync(command.RuleId); + } + + private async Task DeleteRuleAsync(DeleteRule command) + { + var id = command.RuleId; + + var rule = await grainFactory.GetGrain(id).GetStateAsync(); + + if (IsFound(rule.Value)) + { + await Index(rule.Value.AppId.Id).RemoveAsync(id); + } + } + + private IRulesByAppIndexGrain Index(Guid appId) + { + return grainFactory.GetGrain(appId); + } + + private static bool IsFound(IRuleEntity rule) + { + return rule.Version > EtagVersion.Empty && !rule.IsDeleted; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs new file mode 100644 index 000000000..53325adb2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public sealed class ManualTriggerHandler : RuleTriggerHandler + { + protected override Task CreateEnrichedEventAsync(Envelope @event) + { + var result = new EnrichedManualEvent + { + Name = "Manual" + }; + + return Task.FromResult(result); + } + + protected override bool Trigger(EnrichedManualEvent @event, ManualTrigger trigger) + { + return true; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs new file mode 100644 index 000000000..a5ef426f2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Rules.Queries +{ + public sealed class RuleEnricher : IRuleEnricher + { + private readonly IRuleEventRepository ruleEventRepository; + + public RuleEnricher(IRuleEventRepository ruleEventRepository) + { + Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); + + this.ruleEventRepository = ruleEventRepository; + } + + public async Task EnrichAsync(IRuleEntity rule, Context context) + { + Guard.NotNull(rule, nameof(rule)); + + var enriched = await EnrichAsync(Enumerable.Repeat(rule, 1), context); + + return enriched[0]; + } + + public async Task> EnrichAsync(IEnumerable rules, Context context) + { + Guard.NotNull(rules, nameof(rules)); + Guard.NotNull(context, nameof(context)); + + using (Profiler.TraceMethod()) + { + var results = new List(); + + foreach (var rule in rules) + { + var result = SimpleMapper.Map(rule, new RuleEntity()); + + results.Add(result); + } + + foreach (var group in results.GroupBy(x => x.AppId.Id)) + { + var statistics = await ruleEventRepository.QueryStatisticsByAppAsync(group.Key); + + foreach (var rule in group) + { + var statistic = statistics.FirstOrDefault(x => x.RuleId == rule.Id); + + if (statistic != null) + { + rule.LastExecuted = statistic.LastExecuted; + rule.NumFailed = statistic.NumFailed; + rule.NumSucceeded = statistic.NumSucceeded; + + rule.CacheDependencies = new HashSet + { + statistic.LastExecuted + }; + } + } + } + + return results; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs new file mode 100644 index 000000000..d24502f06 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Entities.Rules.Repositories +{ + public interface IRuleEventRepository + { + Task EnqueueAsync(RuleJob job, Instant nextAttempt); + + Task EnqueueAsync(Guid id, Instant nextAttempt); + + Task CancelAsync(Guid id); + + Task MarkSentAsync(RuleJob job, string? dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant finished, Instant? nextCall); + + Task QueryPendingAsync(Instant now, Func callback, CancellationToken ct = default); + + Task CountByAppAsync(Guid appId); + + Task> QueryStatisticsByAppAsync(Guid appId); + + Task> QueryByAppAsync(Guid appId, Guid? ruleId = null, int skip = 0, int take = 20); + + Task FindAsync(Guid id); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/RuleStatistics.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/RuleStatistics.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Repositories/RuleStatistics.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/RuleStatistics.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs new file mode 100644 index 000000000..eb1841bea --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs @@ -0,0 +1,163 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using NodaTime; +using Orleans; +using Orleans.Runtime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public class RuleDequeuerGrain : Grain, IRuleDequeuerGrain, IRemindable + { + private readonly ITargetBlock requestBlock; + private readonly IRuleEventRepository ruleEventRepository; + private readonly RuleService ruleService; + private readonly ConcurrentDictionary executing = new ConcurrentDictionary(); + private readonly IClock clock; + private readonly ISemanticLog log; + + public RuleDequeuerGrain(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log, IClock clock) + { + Guard.NotNull(ruleEventRepository); + Guard.NotNull(ruleService); + Guard.NotNull(clock); + Guard.NotNull(log); + + this.ruleEventRepository = ruleEventRepository; + this.ruleService = ruleService; + + this.clock = clock; + + this.log = log; + + requestBlock = + new PartitionedActionBlock(HandleAsync, x => x.Job.ExecutionPartition, + new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); + } + + public override Task OnActivateAsync() + { + DelayDeactivation(TimeSpan.FromDays(1)); + + RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + RegisterTimer(x => QueryAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); + + return Task.FromResult(true); + } + + public override Task OnDeactivateAsync() + { + requestBlock.Complete(); + + return requestBlock.Completion; + } + + public Task ActivateAsync() + { + return TaskHelper.Done; + } + + public async Task QueryAsync() + { + try + { + var now = clock.GetCurrentInstant(); + + await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "QueueWebhookEvents") + .WriteProperty("status", "Failed")); + } + } + + public async Task HandleAsync(IRuleEventEntity @event) + { + if (!executing.TryAdd(@event.Id, false)) + { + return; + } + + try + { + var job = @event.Job; + + var (response, elapsed) = await ruleService.InvokeAsync(job.ActionName, job.ActionData); + + var jobInvoke = ComputeJobInvoke(response.Status, @event, job); + var jobResult = ComputeJobResult(response.Status, jobInvoke); + + var now = clock.GetCurrentInstant(); + + await ruleEventRepository.MarkSentAsync(@event.Job, response.Dump, response.Status, jobResult, elapsed, now, jobInvoke); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "SendWebhookEvent") + .WriteProperty("status", "Failed")); + } + finally + { + executing.TryRemove(@event.Id, out _); + } + } + + private static RuleJobResult ComputeJobResult(RuleResult result, Instant? nextCall) + { + if (result != RuleResult.Success && !nextCall.HasValue) + { + return RuleJobResult.Failed; + } + else if (result != RuleResult.Success && nextCall.HasValue) + { + return RuleJobResult.Retry; + } + else + { + return RuleJobResult.Success; + } + } + + private static Instant? ComputeJobInvoke(RuleResult result, IRuleEventEntity @event, RuleJob job) + { + if (result != RuleResult.Success) + { + switch (@event.NumCalls) + { + case 0: + return job.Created.Plus(Duration.FromMinutes(5)); + case 1: + return job.Created.Plus(Duration.FromHours(1)); + case 2: + return job.Created.Plus(Duration.FromHours(6)); + case 3: + return job.Created.Plus(Duration.FromHours(12)); + } + } + + return null; + } + + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs new file mode 100644 index 000000000..593685f40 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public sealed class RuleEnqueuer : IEventConsumer, IRuleEnqueuer + { + private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(10); + private readonly IRuleEventRepository ruleEventRepository; + private readonly IAppProvider appProvider; + private readonly IMemoryCache cache; + private readonly RuleService ruleService; + + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return ".*"; } + } + + public RuleEnqueuer(IAppProvider appProvider, IMemoryCache cache, IRuleEventRepository ruleEventRepository, + RuleService ruleService) + { + Guard.NotNull(appProvider); + Guard.NotNull(cache); + Guard.NotNull(ruleEventRepository); + Guard.NotNull(ruleService); + + this.appProvider = appProvider; + + this.cache = cache; + + this.ruleEventRepository = ruleEventRepository; + this.ruleService = ruleService; + } + + public bool Handles(StoredEvent @event) + { + return true; + } + + public Task ClearAsync() + { + return TaskHelper.Done; + } + + public async Task Enqueue(Rule rule, Guid ruleId, Envelope @event) + { + Guard.NotNull(rule, nameof(rule)); + Guard.NotNull(@event, nameof(@event)); + + var job = await ruleService.CreateJobAsync(rule, ruleId, @event); + + if (job != null) + { + await ruleEventRepository.EnqueueAsync(job, job.Created); + } + } + + public async Task On(Envelope @event) + { + if (@event.Payload is AppEvent appEvent) + { + var rules = await GetRulesAsync(appEvent.AppId.Id); + + foreach (var ruleEntity in rules) + { + await Enqueue(ruleEntity.RuleDef, ruleEntity.Id, @event); + } + } + } + + private Task> GetRulesAsync(Guid appId) + { + return cache.GetOrCreateAsync(appId, entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheDuration; + + return appProvider.GetRulesAsync(appId); + }); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs new file mode 100644 index 000000000..8c131db24 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NodaTime; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public sealed class RuleEntity : IEnrichedRuleEntity + { + public Guid Id { get; set; } + + public NamedId AppId { get; set; } + + public NamedId SchemaId { get; set; } + + public long Version { get; set; } + + public Instant Created { get; set; } + + public Instant LastModified { get; set; } + + public RefToken CreatedBy { get; set; } + + public RefToken LastModifiedBy { get; set; } + + public Rule RuleDef { get; set; } + + public bool IsDeleted { get; set; } + + public int NumSucceeded { get; set; } + + public int NumFailed { get; set; } + + public Instant? LastExecuted { get; set; } + + public HashSet CacheDependencies { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs new file mode 100644 index 000000000..02e614bff --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs @@ -0,0 +1,154 @@ +// ========================================================================== +// 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.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.Rules.Guards; +using Squidex.Domain.Apps.Entities.Rules.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public sealed class RuleGrain : DomainObjectGrain, IRuleGrain + { + private readonly IAppProvider appProvider; + private readonly IRuleEnqueuer ruleEnqueuer; + + public RuleGrain(IStore store, ISemanticLog log, IAppProvider appProvider, IRuleEnqueuer ruleEnqueuer) + : base(store, log) + { + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(ruleEnqueuer, nameof(ruleEnqueuer)); + + this.appProvider = appProvider; + + this.ruleEnqueuer = ruleEnqueuer; + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + VerifyNotDeleted(); + + switch (command) + { + case CreateRule createRule: + return CreateReturnAsync(createRule, async c => + { + await GuardRule.CanCreate(c, appProvider); + + Create(c); + + return Snapshot; + }); + case UpdateRule updateRule: + return UpdateReturnAsync(updateRule, async c => + { + await GuardRule.CanUpdate(c, Snapshot.AppId.Id, appProvider, Snapshot.RuleDef); + + Update(c); + + return Snapshot; + }); + case EnableRule enableRule: + return UpdateReturn(enableRule, c => + { + GuardRule.CanEnable(c, Snapshot.RuleDef); + + Enable(c); + + return Snapshot; + }); + case DisableRule disableRule: + return UpdateReturn(disableRule, c => + { + GuardRule.CanDisable(c, Snapshot.RuleDef); + + Disable(c); + + return Snapshot; + }); + case DeleteRule deleteRule: + return Update(deleteRule, c => + { + GuardRule.CanDelete(deleteRule); + + Delete(c); + }); + case TriggerRule triggerRule: + return Trigger(triggerRule); + default: + throw new NotSupportedException(); + } + } + + private async Task Trigger(TriggerRule command) + { + var @event = SimpleMapper.Map(command, new RuleManuallyTriggered { RuleId = Snapshot.Id, AppId = Snapshot.AppId }); + + await ruleEnqueuer.Enqueue(Snapshot.RuleDef, Snapshot.Id, Envelope.Create(@event)); + + return null; + } + + public void Create(CreateRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); + } + + public void Update(UpdateRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleUpdated())); + } + + public void Enable(EnableRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleEnabled())); + } + + public void Disable(DisableRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleDisabled())); + } + + public void Delete(DeleteRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); + } + + private void RaiseEvent(AppEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + RaiseEvent(Envelope.Create(@event)); + } + + private void VerifyNotDeleted() + { + if (Snapshot.IsDeleted) + { + throw new DomainException("Rule has already been deleted."); + } + } + + public Task> GetStateAsync() + { + return J.AsTask(Snapshot); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs new file mode 100644 index 000000000..b96cec588 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + public sealed class UsageTrackerCommandMiddleware : ICommandMiddleware + { + private readonly IUsageTrackerGrain usageTrackerGrain; + + public UsageTrackerCommandMiddleware(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + usageTrackerGrain = grainFactory.GetGrain(SingleGrain.Id); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + switch (context.Command) + { + case DeleteRule deleteRule: + await usageTrackerGrain.RemoveTargetAsync(deleteRule.RuleId); + break; + case CreateRule createRule: + { + if (createRule.Trigger is UsageTrigger usage) + { + await usageTrackerGrain.AddTargetAsync(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays); + } + + break; + } + + case UpdateRule ruleUpdated: + { + if (ruleUpdated.Trigger is UsageTrigger usage) + { + await usageTrackerGrain.UpdateTargetAsync(ruleUpdated.RuleId, usage.Limit, usage.NumDays); + } + + break; + } + } + + await next(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs new file mode 100644 index 000000000..16dccf158 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -0,0 +1,158 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Orleans; +using Orleans.Concurrency; +using Orleans.Runtime; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.UsageTracking; + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + [Reentrant] + public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain + { + private readonly IGrainState state; + private readonly IUsageTracker usageTracker; + + public sealed class Target + { + public NamedId AppId { get; set; } + + public int Limits { get; set; } + + public int? NumDays { get; set; } + + public DateTime? Triggered { get; set; } + } + + [CollectionName("UsageTracker")] + public sealed class GrainState + { + public Dictionary Targets { get; set; } = new Dictionary(); + } + + public UsageTrackerGrain(IGrainState state, IUsageTracker usageTracker) + { + Guard.NotNull(state); + Guard.NotNull(usageTracker); + + this.state = state; + + this.usageTracker = usageTracker; + } + + protected override Task OnActivateAsync(string key) + { + DelayDeactivation(TimeSpan.FromDays(1)); + + RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + RegisterTimer(x => CheckUsagesAsync(), null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); + + return TaskHelper.Done; + } + + public Task ActivateAsync() + { + return TaskHelper.Done; + } + + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return TaskHelper.Done; + } + + public async Task CheckUsagesAsync() + { + var today = DateTime.Today; + + foreach (var kvp in state.Value.Targets) + { + var target = kvp.Value; + + var from = GetFromDate(today, target.NumDays); + + if (!target.Triggered.HasValue || target.Triggered < from) + { + var usage = await usageTracker.GetMonthlyCallsAsync(target.AppId.Id.ToString(), today); + + var limit = kvp.Value.Limits; + + if (usage > limit) + { + kvp.Value.Triggered = today; + + var @event = new AppUsageExceeded + { + AppId = target.AppId, + CallsCurrent = usage, + CallsLimit = limit, + RuleId = kvp.Key + }; + + await state.WriteEventAsync(Envelope.Create(@event)); + } + } + } + + await state.WriteAsync(); + } + + private static DateTime GetFromDate(DateTime today, int? numDays) + { + if (numDays.HasValue) + { + return today.AddDays(-numDays.Value).AddDays(1); + } + else + { + return new DateTime(today.Year, today.Month, 1); + } + } + + public Task AddTargetAsync(Guid ruleId, NamedId appId, int limits, int? numDays) + { + UpdateTarget(ruleId, t => { t.Limits = limits; t.AppId = appId; t.NumDays = numDays; }); + + return state.WriteAsync(); + } + + public Task UpdateTargetAsync(Guid ruleId, int limits, int? numDays) + { + UpdateTarget(ruleId, t => { t.Limits = limits; t.NumDays = numDays; }); + + return state.WriteAsync(); + } + + public Task AddTargetAsync(Guid ruleId, int limits) + { + UpdateTarget(ruleId, t => t.Limits = limits); + + return state.WriteAsync(); + } + + public Task RemoveTargetAsync(Guid ruleId) + { + state.Value.Targets.Remove(ruleId); + + return state.WriteAsync(); + } + + private void UpdateTarget(Guid ruleId, Action updater) + { + updater(state.Value.Targets.GetOrAddNew(ruleId)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs new file mode 100644 index 000000000..ae2154284 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + public sealed class UsageTriggerHandler : RuleTriggerHandler + { + private const string EventName = "Usage exceeded"; + + protected override Task CreateEnrichedEventAsync(Envelope @event) + { + var result = new EnrichedUsageExceededEvent + { + CallsCurrent = @event.Payload.CallsCurrent, + CallsLimit = @event.Payload.CallsLimit, + Name = EventName + }; + + return Task.FromResult(result); + } + + protected override bool Trigger(EnrichedUsageExceededEvent @event, UsageTrigger trigger) + { + return @event.CallsLimit == trigger.Limit; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs new file mode 100644 index 000000000..a62306c64 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Domain.Apps.Entities.Schemas.Indexes; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public sealed class BackupSchemas : BackupHandler + { + private readonly Dictionary schemasByName = new Dictionary(); + private readonly ISchemasIndex indexSchemas; + + public override string Name { get; } = "Schemas"; + + public BackupSchemas(ISchemasIndex indexSchemas) + { + Guard.NotNull(indexSchemas); + + this.indexSchemas = indexSchemas; + } + + public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) + { + switch (@event.Payload) + { + case SchemaCreated schemaCreated: + schemasByName[schemaCreated.SchemaId.Name] = schemaCreated.SchemaId.Id; + break; + case SchemaDeleted schemaDeleted: + schemasByName.Remove(schemaDeleted.SchemaId.Name); + break; + } + + return TaskHelper.True; + } + + public override Task RestoreAsync(Guid appId, BackupReader reader) + { + return indexSchemas.RebuildAsync(appId, schemasByName); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardHelper.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardHelper.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardHelper.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardHelper.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs new file mode 100644 index 000000000..a009957f8 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs @@ -0,0 +1,251 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +#pragma warning disable IDE0060 // Remove unused parameter + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards +{ + public static class GuardSchema + { + public static void CanCreate(CreateSchema command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot create schema.", e => + { + if (!command.Name.IsSlug()) + { + e(Not.ValidSlug("Name"), nameof(command.Name)); + } + + ValidateUpsert(command, e); + }); + } + + public static void CanSynchronize(SynchronizeSchema command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot synchronize schema.", e => + { + ValidateUpsert(command, e); + }); + } + + public static void CanReorder(Schema schema, ReorderFields command) + { + Guard.NotNull(command); + + IArrayField? arrayField = null; + + if (command.ParentFieldId.HasValue) + { + arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false); + } + + Validate.It(() => "Cannot reorder schema fields.", error => + { + if (command.FieldIds == null) + { + error("Field ids is required.", nameof(command.FieldIds)); + } + + if (arrayField == null) + { + ValidateFieldIds(error, command, schema.FieldsById); + } + else + { + ValidateFieldIds(error, command, arrayField.FieldsById); + } + }); + } + + public static void CanConfigurePreviewUrls(ConfigurePreviewUrls command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot configure preview urls.", error => + { + if (command.PreviewUrls == null) + { + error("Preview Urls is required.", nameof(command.PreviewUrls)); + } + }); + } + + public static void CanPublish(Schema schema, PublishSchema command) + { + Guard.NotNull(command); + + if (schema.IsPublished) + { + throw new DomainException("Schema is already published."); + } + } + + public static void CanUnpublish(Schema schema, UnpublishSchema command) + { + Guard.NotNull(command); + + if (!schema.IsPublished) + { + throw new DomainException("Schema is not published."); + } + } + + public static void CanUpdate(Schema schema, UpdateSchema command) + { + Guard.NotNull(command); + } + + public static void CanConfigureScripts(Schema schema, ConfigureScripts command) + { + Guard.NotNull(command); + } + + public static void CanChangeCategory(Schema schema, ChangeCategory command) + { + Guard.NotNull(command); + } + + public static void CanDelete(Schema schema, DeleteSchema command) + { + Guard.NotNull(command); + } + + private static void ValidateUpsert(UpsertCommand command, AddValidation e) + { + if (command.Fields?.Count > 0) + { + var fieldIndex = 0; + var fieldPrefix = string.Empty; + + foreach (var field in command.Fields) + { + fieldIndex++; + fieldPrefix = $"Fields[{fieldIndex}]"; + + ValidateRootField(field, fieldPrefix, e); + } + + if (command.Fields.Select(x => x?.Name).Distinct().Count() != command.Fields.Count) + { + e("Fields cannot have duplicate names.", nameof(command.Fields)); + } + } + } + + private static void ValidateRootField(UpsertSchemaField field, string prefix, AddValidation e) + { + if (field == null) + { + e(Not.Defined("Field"), prefix); + } + else + { + if (!field.Partitioning.IsValidPartitioning()) + { + e(Not.Valid("Partitioning"), $"{prefix}.{nameof(field.Partitioning)}"); + } + + ValidateField(field, prefix, e); + + if (field.Nested?.Count > 0) + { + if (field.Properties is ArrayFieldProperties) + { + var nestedIndex = 0; + var nestedPrefix = string.Empty; + + foreach (var nestedField in field.Nested) + { + nestedIndex++; + nestedPrefix = $"{prefix}.Nested[{nestedIndex}]"; + + ValidateNestedField(nestedField, nestedPrefix, e); + } + } + else if (field.Nested.Count > 0) + { + e("Only array fields can have nested fields.", $"{prefix}.{nameof(field.Partitioning)}"); + } + + if (field.Nested.Select(x => x.Name).Distinct().Count() != field.Nested.Count) + { + e("Fields cannot have duplicate names.", $"{prefix}.Nested"); + } + } + } + } + + private static void ValidateNestedField(UpsertSchemaNestedField nestedField, string prefix, AddValidation e) + { + if (nestedField == null) + { + e(Not.Defined("Field"), prefix); + } + else + { + if (nestedField.Properties is ArrayFieldProperties) + { + e("Nested field cannot be array fields.", $"{prefix}.{nameof(nestedField.Properties)}"); + } + + ValidateField(nestedField, prefix, e); + } + } + + private static void ValidateField(UpsertSchemaFieldBase field, string prefix, AddValidation e) + { + if (!field.Name.IsPropertyName()) + { + e("Field name must be a valid javascript property name.", $"{prefix}.{nameof(field.Name)}"); + } + + if (field.Properties == null) + { + e(Not.Defined("Field properties"), $"{prefix}.{nameof(field.Properties)}"); + } + else + { + if (!field.Properties.IsForApi()) + { + if (field.IsHidden) + { + e("UI field cannot be hidden.", $"{prefix}.{nameof(field.IsHidden)}"); + } + + if (field.IsDisabled) + { + e("UI field cannot be disabled.", $"{prefix}.{nameof(field.IsDisabled)}"); + } + } + + var errors = FieldPropertiesValidator.Validate(field.Properties); + + errors.Foreach(x => x.WithPrefix($"{prefix}.{nameof(field.Properties)}").AddTo(e)); + } + } + + private static void ValidateFieldIds(AddValidation error, ReorderFields c, IReadOnlyDictionary fields) + { + if (c.FieldIds != null && (c.FieldIds.Count != fields.Count || c.FieldIds.Any(x => !fields.ContainsKey(x)))) + { + error("Field ids do not cover all fields.", nameof(c.FieldIds)); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs new file mode 100644 index 000000000..f99ffd53d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs @@ -0,0 +1,167 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards +{ + public static class GuardSchemaField + { + public static void CanAdd(Schema schema, AddField command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot add a new field.", e => + { + if (!command.Name.IsPropertyName()) + { + e("Name must be a valid javascript property name.", nameof(command.Name)); + } + + if (command.Properties == null) + { + e(Not.Defined("Properties"), nameof(command.Properties)); + } + else + { + var errors = FieldPropertiesValidator.Validate(command.Properties); + + errors.Foreach(x => x.WithPrefix(nameof(command.Properties)).AddTo(e)); + } + + if (command.ParentFieldId.HasValue) + { + var arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false); + + if (arrayField.FieldsByName.ContainsKey(command.Name)) + { + e("A field with the same name already exists."); + } + } + else + { + if (command.ParentFieldId == null && !command.Partitioning.IsValidPartitioning()) + { + e(Not.Valid("Partitioning"), nameof(command.Partitioning)); + } + + if (schema.FieldsByName.ContainsKey(command.Name)) + { + e("A field with the same name already exists."); + } + } + }); + } + + public static void CanUpdate(Schema schema, UpdateField command) + { + Guard.NotNull(command); + + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + + Validate.It(() => "Cannot update field.", e => + { + if (command.Properties == null) + { + e(Not.Defined("Properties"), nameof(command.Properties)); + } + else + { + var errors = FieldPropertiesValidator.Validate(command.Properties); + + errors.Foreach(x => x.WithPrefix(nameof(command.Properties)).AddTo(e)); + } + }); + } + + public static void CanHide(Schema schema, HideField command) + { + Guard.NotNull(command); + + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + + if (field.IsHidden) + { + throw new DomainException("Schema field is already hidden."); + } + + if (!field.IsForApi()) + { + throw new DomainException("UI field cannot be hidden."); + } + } + + public static void CanShow(Schema schema, ShowField command) + { + Guard.NotNull(command); + + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + + if (!field.IsHidden) + { + throw new DomainException("Schema field is already visible."); + } + } + + public static void CanDisable(Schema schema, DisableField command) + { + Guard.NotNull(command); + + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + + if (field.IsDisabled) + { + throw new DomainException("Schema field is already disabled."); + } + + if (!field.IsForApi(true)) + { + throw new DomainException("UI field cannot be disabled."); + } + } + + public static void CanDelete(Schema schema, DeleteField command) + { + Guard.NotNull(command); + + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + + if (field.IsLocked) + { + throw new DomainException("Schema field is locked."); + } + } + + public static void CanEnable(Schema schema, EnableField command) + { + Guard.NotNull(command); + + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + + if (!field.IsDisabled) + { + throw new DomainException("Schema field is already enabled."); + } + } + + public static void CanLock(Schema schema, LockField command) + { + Guard.NotNull(command); + + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + + if (field.IsLocked) + { + throw new DomainException("Schema field is already locked."); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/ISchemaGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs new file mode 100644 index 000000000..0031ffe31 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public interface ISchemasIndex + { + Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false); + + Task GetSchemaByNameAsync(Guid appId, string name, bool allowDeleted = false); + + Task> GetSchemasAsync(Guid appId, bool allowDeleted = false); + + Task RebuildAsync(Guid appId, Dictionary schemas); + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs new file mode 100644 index 000000000..a0159e0d9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs @@ -0,0 +1,181 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex + { + private readonly IGrainFactory grainFactory; + + public SchemasIndex(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.grainFactory = grainFactory; + } + + public Task RebuildAsync(Guid appId, Dictionary schemas) + { + return Index(appId).RebuildAsync(schemas); + } + + public async Task> GetSchemasAsync(Guid appId, bool allowDeleted = false) + { + using (Profiler.TraceMethod()) + { + var ids = await GetSchemaIdsAsync(appId); + + var schemas = + await Task.WhenAll( + ids.Select(id => GetSchemaAsync(appId, id, allowDeleted))); + + return schemas.Where(x => x != null).ToList(); + } + } + + public async Task GetSchemaByNameAsync(Guid appId, string name, bool allowDeleted = false) + { + using (Profiler.TraceMethod()) + { + var id = await GetSchemaIdAsync(appId, name); + + if (id == default) + { + return null; + } + + return await GetSchemaAsync(appId, id, allowDeleted); + } + } + + public async Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) + { + using (Profiler.TraceMethod()) + { + var schema = await grainFactory.GetGrain(id).GetStateAsync(); + + if (IsFound(schema.Value, allowDeleted)) + { + return schema.Value; + } + + return null; + } + } + + private async Task GetSchemaIdAsync(Guid appId, string name) + { + using (Profiler.TraceMethod()) + { + return await Index(appId).GetIdAsync(name); + } + } + + private async Task> GetSchemaIdsAsync(Guid appId) + { + using (Profiler.TraceMethod()) + { + return await Index(appId).GetIdsAsync(); + } + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.Command is CreateSchema createSchema) + { + var index = Index(createSchema.AppId.Id); + + var token = await CheckSchemaAsync(index, createSchema); + + try + { + await next(); + } + finally + { + if (token != null) + { + if (context.IsCompleted) + { + await index.AddAsync(token); + } + else + { + await index.RemoveReservationAsync(token); + } + } + } + } + else + { + await next(); + + if (context.IsCompleted) + { + if (context.Command is DeleteSchema deleteSchema) + { + await DeleteSchemaAsync(deleteSchema); + } + } + } + } + + private async Task CheckSchemaAsync(ISchemasByAppIndexGrain index, CreateSchema command) + { + var name = command.Name; + + if (name.IsSlug()) + { + var token = await index.ReserveAsync(command.SchemaId, name); + + if (token == null) + { + var error = new ValidationError("A schema with this name already exists."); + + throw new ValidationException("Cannot create schema.", error); + } + + return token; + } + + return null; + } + + private async Task DeleteSchemaAsync(DeleteSchema commmand) + { + var schemaId = commmand.SchemaId; + + var schema = await grainFactory.GetGrain(schemaId).GetStateAsync(); + + if (IsFound(schema.Value, true)) + { + await Index(schema.Value.AppId.Id).RemoveAsync(schemaId); + } + } + + private ISchemasByAppIndexGrain Index(Guid appId) + { + return grainFactory.GetGrain(appId); + } + + private static bool IsFound(ISchemaEntity entity, bool allowDeleted) + { + return entity.Version > EtagVersion.Empty && (!entity.IsDeleted || allowDeleted); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs new file mode 100644 index 000000000..f78db7adb --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public sealed class SchemaChangedTriggerHandler : RuleTriggerHandler + { + private readonly IScriptEngine scriptEngine; + + public SchemaChangedTriggerHandler(IScriptEngine scriptEngine) + { + Guard.NotNull(scriptEngine); + + this.scriptEngine = scriptEngine; + } + + protected override Task CreateEnrichedEventAsync(Envelope @event) + { + EnrichedSchemaEvent? result = new EnrichedSchemaEvent(); + + SimpleMapper.Map(@event.Payload, result); + + switch (@event.Payload) + { + case FieldEvent _: + case SchemaPreviewUrlsConfigured _: + case SchemaScriptsConfigured _: + case SchemaUpdated _: + case ParentFieldEvent _: + result.Type = EnrichedSchemaEventType.Updated; + break; + case SchemaCreated _: + result.Type = EnrichedSchemaEventType.Created; + break; + case SchemaPublished _: + result.Type = EnrichedSchemaEventType.Published; + break; + case SchemaUnpublished _: + result.Type = EnrichedSchemaEventType.Unpublished; + break; + case SchemaDeleted _: + result.Type = EnrichedSchemaEventType.Deleted; + break; + default: + result = null; + break; + } + + if (result != null) + { + result.Name = $"Schema{result.Type}"; + } + + return Task.FromResult(result); + } + + protected override bool Trigger(EnrichedSchemaEvent @event, SchemaChangedTrigger trigger) + { + return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs new file mode 100644 index 000000000..f4fecddfc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs @@ -0,0 +1,417 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.EventSynchronization; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Domain.Apps.Entities.Schemas.Guards; +using Squidex.Domain.Apps.Entities.Schemas.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public sealed class SchemaGrain : DomainObjectGrain, ISchemaGrain + { + private readonly IJsonSerializer serializer; + + public SchemaGrain(IStore store, ISemanticLog log, IJsonSerializer serializer) + : base(store, log) + { + Guard.NotNull(serializer); + + this.serializer = serializer; + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + VerifyNotDeleted(); + + switch (command) + { + case AddField addField: + return UpdateReturn(addField, c => + { + GuardSchemaField.CanAdd(Snapshot.SchemaDef, c); + + Add(c); + + long id; + + if (c.ParentFieldId == null) + { + id = Snapshot.SchemaDef.FieldsByName[c.Name].Id; + } + else + { + id = ((IArrayField)Snapshot.SchemaDef.FieldsById[c.ParentFieldId.Value]).FieldsByName[c.Name].Id; + } + + return Snapshot; + }); + + case CreateSchema createSchema: + return CreateReturn(createSchema, c => + { + GuardSchema.CanCreate(c); + + Create(c); + + return Snapshot; + }); + + case SynchronizeSchema synchronizeSchema: + return UpdateReturn(synchronizeSchema, c => + { + GuardSchema.CanSynchronize(c); + + Synchronize(c); + + return Snapshot; + }); + + case DeleteField deleteField: + return UpdateReturn(deleteField, c => + { + GuardSchemaField.CanDelete(Snapshot.SchemaDef, deleteField); + + DeleteField(c); + + return Snapshot; + }); + + case LockField lockField: + return UpdateReturn(lockField, c => + { + GuardSchemaField.CanLock(Snapshot.SchemaDef, lockField); + + LockField(c); + + return Snapshot; + }); + + case HideField hideField: + return UpdateReturn(hideField, c => + { + GuardSchemaField.CanHide(Snapshot.SchemaDef, c); + + HideField(c); + + return Snapshot; + }); + + case ShowField showField: + return UpdateReturn(showField, c => + { + GuardSchemaField.CanShow(Snapshot.SchemaDef, c); + + ShowField(c); + + return Snapshot; + }); + + case DisableField disableField: + return UpdateReturn(disableField, c => + { + GuardSchemaField.CanDisable(Snapshot.SchemaDef, c); + + DisableField(c); + + return Snapshot; + }); + + case EnableField enableField: + return UpdateReturn(enableField, c => + { + GuardSchemaField.CanEnable(Snapshot.SchemaDef, c); + + EnableField(c); + + return Snapshot; + }); + + case UpdateField updateField: + return UpdateReturn(updateField, c => + { + GuardSchemaField.CanUpdate(Snapshot.SchemaDef, c); + + UpdateField(c); + + return Snapshot; + }); + + case ReorderFields reorderFields: + return UpdateReturn(reorderFields, c => + { + GuardSchema.CanReorder(Snapshot.SchemaDef, c); + + Reorder(c); + + return Snapshot; + }); + + case UpdateSchema updateSchema: + return UpdateReturn(updateSchema, c => + { + GuardSchema.CanUpdate(Snapshot.SchemaDef, c); + + Update(c); + + return Snapshot; + }); + + case PublishSchema publishSchema: + return UpdateReturn(publishSchema, c => + { + GuardSchema.CanPublish(Snapshot.SchemaDef, c); + + Publish(c); + + return Snapshot; + }); + + case UnpublishSchema unpublishSchema: + return UpdateReturn(unpublishSchema, c => + { + GuardSchema.CanUnpublish(Snapshot.SchemaDef, c); + + Unpublish(c); + + return Snapshot; + }); + + case ConfigureScripts configureScripts: + return UpdateReturn(configureScripts, c => + { + GuardSchema.CanConfigureScripts(Snapshot.SchemaDef, c); + + ConfigureScripts(c); + + return Snapshot; + }); + + case ChangeCategory changeCategory: + return UpdateReturn(changeCategory, c => + { + GuardSchema.CanChangeCategory(Snapshot.SchemaDef, c); + + ChangeCategory(c); + + return Snapshot; + }); + + case ConfigurePreviewUrls configurePreviewUrls: + return UpdateReturn(configurePreviewUrls, c => + { + GuardSchema.CanConfigurePreviewUrls(c); + + ConfigurePreviewUrls(c); + + return Snapshot; + }); + + case DeleteSchema deleteSchema: + return Update(deleteSchema, c => + { + GuardSchema.CanDelete(Snapshot.SchemaDef, c); + + Delete(c); + }); + + default: + throw new NotSupportedException(); + } + } + + public void Synchronize(SynchronizeSchema command) + { + var options = new SchemaSynchronizationOptions + { + NoFieldDeletion = command.NoFieldDeletion, + NoFieldRecreation = command.NoFieldRecreation + }; + + var schemaSource = Snapshot.SchemaDef; + var schemaTarget = command.ToSchema(schemaSource.Name, schemaSource.IsSingleton); + + var events = schemaSource.Synchronize(schemaTarget, serializer, () => Snapshot.SchemaFieldsTotal + 1, options); + + foreach (var @event in events) + { + RaiseEvent(SimpleMapper.Map(command, (SchemaEvent)@event)); + } + } + + public void Create(CreateSchema command) + { + RaiseEvent(command, new SchemaCreated { SchemaId = NamedId.Of(command.SchemaId, command.Name), Schema = command.ToSchema() }); + } + + public void Add(AddField command) + { + RaiseEvent(command, new FieldAdded { FieldId = CreateFieldId(command) }); + } + + public void UpdateField(UpdateField command) + { + RaiseEvent(command, new FieldUpdated()); + } + + public void LockField(LockField command) + { + RaiseEvent(command, new FieldLocked()); + } + + public void HideField(HideField command) + { + RaiseEvent(command, new FieldHidden()); + } + + public void ShowField(ShowField command) + { + RaiseEvent(command, new FieldShown()); + } + + public void DisableField(DisableField command) + { + RaiseEvent(command, new FieldDisabled()); + } + + public void EnableField(EnableField command) + { + RaiseEvent(command, new FieldEnabled()); + } + + public void DeleteField(DeleteField command) + { + RaiseEvent(command, new FieldDeleted()); + } + + public void Reorder(ReorderFields command) + { + RaiseEvent(command, new SchemaFieldsReordered()); + } + + public void Publish(PublishSchema command) + { + RaiseEvent(command, new SchemaPublished()); + } + + public void Unpublish(UnpublishSchema command) + { + RaiseEvent(command, new SchemaUnpublished()); + } + + public void ConfigureScripts(ConfigureScripts command) + { + RaiseEvent(command, new SchemaScriptsConfigured()); + } + + public void ChangeCategory(ChangeCategory command) + { + RaiseEvent(command, new SchemaCategoryChanged()); + } + + public void ConfigurePreviewUrls(ConfigurePreviewUrls command) + { + RaiseEvent(command, new SchemaPreviewUrlsConfigured()); + } + + public void Update(UpdateSchema command) + { + RaiseEvent(command, new SchemaUpdated()); + } + + public void Delete(DeleteSchema command) + { + RaiseEvent(command, new SchemaDeleted()); + } + + private void RaiseEvent(TCommand command, TEvent @event) where TCommand : SchemaCommand where TEvent : SchemaEvent + { + SimpleMapper.Map(command, @event); + + NamedId? GetFieldId(long? id) + { + if (id.HasValue && Snapshot.SchemaDef.FieldsById.TryGetValue(id.Value, out var field)) + { + return field.NamedId(); + } + + return null; + } + + if (command is ParentFieldCommand pc && @event is ParentFieldEvent pe) + { + if (pc.ParentFieldId.HasValue) + { + if (Snapshot.SchemaDef.FieldsById.TryGetValue(pc.ParentFieldId.Value, out var field)) + { + pe.ParentFieldId = field.NamedId(); + + if (command is FieldCommand fc && @event is FieldEvent fe) + { + if (field is IArrayField arrayField && arrayField.FieldsById.TryGetValue(fc.FieldId, out var nestedField)) + { + fe.FieldId = nestedField.NamedId(); + } + } + } + } + else if (command is FieldCommand fc && @event is FieldEvent fe) + { + fe.FieldId = GetFieldId(fc.FieldId)!; + } + } + + RaiseEvent(@event); + } + + private void RaiseEvent(SchemaEvent @event) + { + if (@event.SchemaId == null) + { + @event.SchemaId = Snapshot.NamedId(); + } + + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + RaiseEvent(Envelope.Create(@event)); + } + + private NamedId CreateFieldId(AddField command) + { + return NamedId.Of(Snapshot.SchemaFieldsTotal + 1, command.Name); + } + + private void VerifyNotDeleted() + { + if (Snapshot.IsDeleted) + { + throw new DomainException("Schema has already been deleted."); + } + } + + public Task> GetStateAsync() + { + return J.AsTask(Snapshot); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs new file mode 100644 index 000000000..2807562d1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs @@ -0,0 +1,93 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public sealed class SchemaHistoryEventsCreator : HistoryEventsCreatorBase + { + public SchemaHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) + { + AddEventMessage( + "reordered fields of schema {[Name]}."); + + AddEventMessage( + "created schema {[Name]}."); + + AddEventMessage( + "updated schema {[Name]}."); + + AddEventMessage( + "deleted schema {[Name]}."); + + AddEventMessage( + "published schema {[Name]}."); + + AddEventMessage( + "unpublished schema {[Name]}."); + + AddEventMessage( + "reordered fields of schema {[Name]}."); + + AddEventMessage( + "configured script of schema {[Name]}."); + + AddEventMessage( + "added field {[Field]} to schema {[Name]}."); + + AddEventMessage( + "deleted field {[Field]} from schema {[Name]}."); + + AddEventMessage( + "has locked field {[Field]} of schema {[Name]}."); + + AddEventMessage( + "has hidden field {[Field]} of schema {[Name]}."); + + AddEventMessage( + "has shown field {[Field]} of schema {[Name]}."); + + AddEventMessage( + "disabled field {[Field]} of schema {[Name]}."); + + AddEventMessage( + "disabled field {[Field]} of schema {[Name]}."); + + AddEventMessage( + "has updated field {[Field]} of schema {[Name]}."); + + AddEventMessage( + "deleted field {[Field]} of schema {[Name]}."); + } + + protected override Task CreateEventCoreAsync(Envelope @event) + { + HistoryEvent? result = null; + + if (@event.Payload is SchemaEvent schemaEvent) + { + var channel = $"schemas.{schemaEvent.SchemaId.Name}"; + + result = ForEvent(@event.Payload, channel).Param("Name", schemaEvent.SchemaId.Name); + + if (schemaEvent is FieldEvent fieldEvent) + { + result.Param("Field", fieldEvent.FieldId.Name); + } + } + + return Task.FromResult(result); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj new file mode 100644 index 000000000..0deed8bfd --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -0,0 +1,41 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/SquidexCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs b/backend/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/SquidexEntities.cs rename to backend/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs diff --git a/src/Squidex.Domain.Apps.Entities/SquidexEventEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/SquidexEventEnricher.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/SquidexEventEnricher.cs rename to backend/src/Squidex.Domain.Apps.Entities/SquidexEventEnricher.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs b/backend/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs new file mode 100644 index 000000000..6efb9e4d6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Tags +{ + public sealed class GrainTagService : ITagService + { + private readonly IGrainFactory grainFactory; + + public string Name + { + get { return "Tags"; } + } + + public GrainTagService(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public Task> NormalizeTagsAsync(Guid appId, string group, HashSet? names, HashSet? ids) + { + return GetGrain(appId, group).NormalizeTagsAsync(names, ids); + } + + public Task> GetTagIdsAsync(Guid appId, string group, HashSet names) + { + return GetGrain(appId, group).GetTagIdsAsync(names); + } + + public Task> DenormalizeTagsAsync(Guid appId, string group, HashSet ids) + { + return GetGrain(appId, group).DenormalizeTagsAsync(ids); + } + + public Task GetTagsAsync(Guid appId, string group) + { + return GetGrain(appId, group).GetTagsAsync(); + } + + public Task GetExportableTagsAsync(Guid appId, string group) + { + return GetGrain(appId, group).GetExportableTagsAsync(); + } + + public Task RebuildTagsAsync(Guid appId, string group, TagsExport tags) + { + return GetGrain(appId, group).RebuildAsync(tags); + } + + public Task ClearAsync(Guid appId, string group) + { + return GetGrain(appId, group).ClearAsync(); + } + + private ITagGrain GetGrain(Guid appId, string group) + { + Guard.NotNullOrEmpty(group); + + return grainFactory.GetGrain($"{appId}_{group}"); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Tags/ITagGenerator.cs b/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGenerator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Tags/ITagGenerator.cs rename to backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGenerator.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs new file mode 100644 index 000000000..a1cd13b9d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// 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 Orleans; +using Squidex.Domain.Apps.Core.Tags; + +namespace Squidex.Domain.Apps.Entities.Tags +{ + public interface ITagGrain : IGrainWithStringKey + { + Task> NormalizeTagsAsync(HashSet? names, HashSet? ids); + + Task> GetTagIdsAsync(HashSet names); + + Task> DenormalizeTagsAsync(HashSet ids); + + Task GetTagsAsync(); + + Task GetExportableTagsAsync(); + + Task ClearAsync(); + + Task RebuildAsync(TagsExport tags); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs new file mode 100644 index 000000000..32240fbdc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs @@ -0,0 +1,152 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Tags +{ + public sealed class TagGrain : GrainOfString, ITagGrain + { + private readonly IGrainState state; + + [CollectionName("Index_Tags")] + public sealed class GrainState + { + public TagsExport Tags { get; set; } = new TagsExport(); + } + + public TagGrain(IGrainState state) + { + Guard.NotNull(state); + + this.state = state; + } + + public Task ClearAsync() + { + return state.ClearAsync(); + } + + public Task RebuildAsync(TagsExport tags) + { + state.Value.Tags = tags; + + return state.WriteAsync(); + } + + public async Task> NormalizeTagsAsync(HashSet? names, HashSet? ids) + { + var result = new Dictionary(); + + if (names != null) + { + foreach (var tag in names) + { + if (!string.IsNullOrWhiteSpace(tag)) + { + var tagName = tag.ToLowerInvariant(); + var tagId = string.Empty; + + var found = state.Value.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, tagName, StringComparison.OrdinalIgnoreCase)); + + if (found.Value != null) + { + tagId = found.Key; + + if (ids == null || !ids.Contains(tagId)) + { + found.Value.Count++; + } + } + else + { + tagId = Guid.NewGuid().ToString(); + + state.Value.Tags.Add(tagId, new Tag { Name = tagName }); + } + + result.Add(tagName, tagId); + } + } + } + + if (ids != null) + { + foreach (var id in ids) + { + if (!result.ContainsValue(id)) + { + if (state.Value.Tags.TryGetValue(id, out var tagInfo)) + { + tagInfo.Count--; + + if (tagInfo.Count <= 0) + { + state.Value.Tags.Remove(id); + } + } + } + } + } + + await state.WriteAsync(); + + return result; + } + + public Task> GetTagIdsAsync(HashSet names) + { + var result = new Dictionary(); + + foreach (var name in names) + { + var id = state.Value.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, name, StringComparison.OrdinalIgnoreCase)).Key; + + if (!string.IsNullOrWhiteSpace(id)) + { + result.Add(name, id); + } + } + + return Task.FromResult(result); + } + + public Task> DenormalizeTagsAsync(HashSet ids) + { + var result = new Dictionary(); + + foreach (var id in ids) + { + if (state.Value.Tags.TryGetValue(id, out var tagInfo)) + { + result[id] = tagInfo.Name; + } + } + + return Task.FromResult(result); + } + + public Task GetTagsAsync() + { + var tags = state.Value.Tags.Values.ToDictionary(x => x.Name, x => x.Count); + + return Task.FromResult(new TagsSet(tags, state.Version)); + } + + public Task GetExportableTagsAsync() + { + return Task.FromResult(state.Value.Tags); + } + } +} diff --git a/src/Squidex.Domain.Apps.Events/AppEvent.cs b/backend/src/Squidex.Domain.Apps.Events/AppEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/AppEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/AppEvent.cs diff --git a/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs b/backend/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs rename to backend/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppArchived.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppArchived.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppArchived.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppArchived.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppClientAttached.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientAttached.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppClientAttached.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppClientAttached.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppClientRevoked.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRevoked.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppClientRevoked.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRevoked.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppCreated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppCreated.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppCreated.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppImageRemoved.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppImageRemoved.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppImageRemoved.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppImageRemoved.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppImageUploaded.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppImageUploaded.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppImageUploaded.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppImageUploaded.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppLanguageAdded.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageAdded.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppLanguageAdded.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageAdded.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppLanguageRemoved.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageRemoved.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppLanguageRemoved.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageRemoved.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppMasterLanguageSet.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppMasterLanguageSet.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppMasterLanguageSet.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppMasterLanguageSet.cs diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs new file mode 100644 index 000000000..8635344e5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Apps +{ + [EventType(nameof(AppPatternAdded))] + public sealed class AppPatternAdded : AppEvent + { + public Guid PatternId { get; set; } + + public string Name { get; set; } + + public string Pattern { get; set; } + + public string? Message { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs new file mode 100644 index 000000000..cc898cc6c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Apps +{ + [EventType(nameof(AppPatternUpdated))] + public sealed class AppPatternUpdated : AppEvent + { + public Guid PatternId { get; set; } + + public string Name { get; set; } + + public string Pattern { get; set; } + + public string? Message { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppPlanReset.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanReset.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppPlanReset.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanReset.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs new file mode 100644 index 000000000..b5ecfcb00 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Assets +{ + [EventType(nameof(AssetAnnotated))] + public sealed class AssetAnnotated : AssetEvent + { + public string FileName { get; set; } + + public string Slug { get; set; } + + public HashSet? Tags { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs new file mode 100644 index 000000000..ef7173832 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Assets +{ + [EventType(nameof(AssetCreated))] + public sealed class AssetCreated : AssetEvent + { + public string FileName { get; set; } + + public string FileHash { get; set; } + + public string MimeType { get; set; } + + public string Slug { get; set; } + + public long FileVersion { get; set; } + + public long FileSize { get; set; } + + public bool IsImage { get; set; } + + public int? PixelWidth { get; set; } + + public int? PixelHeight { get; set; } + + public HashSet? Tags { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Assets/AssetEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Assets/AssetEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/Assets/AssetEvent.cs diff --git a/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs rename to backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs diff --git a/src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentChangesDiscarded.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentChangesDiscarded.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentChangesDiscarded.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentChangesDiscarded.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentChangesPublished.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentChangesPublished.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentChangesPublished.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentChangesPublished.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentEvent.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentUpdateProposed.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentUpdateProposed.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentUpdateProposed.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentUpdateProposed.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/SchemaEvent.cs b/backend/src/Squidex.Domain.Apps.Events/SchemaEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/SchemaEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/SchemaEvent.cs diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs new file mode 100644 index 000000000..3bec00f1a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Schemas +{ + [EventType(nameof(FieldAdded))] + public sealed class FieldAdded : FieldEvent + { + public string Name { get; set; } + + public string? Partitioning { get; set; } + + public FieldProperties Properties { get; set; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs new file mode 100644 index 000000000..01bcb1e5c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Events.Schemas +{ + public abstract class ParentFieldEvent : SchemaEvent + { + public NamedId? ParentFieldId { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs diff --git a/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj new file mode 100644 index 000000000..d363a9005 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj @@ -0,0 +1,29 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/src/Squidex.Domain.Apps.Events/SquidexEvent.cs b/backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/SquidexEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs diff --git a/src/Squidex.Domain.Apps.Events/SquidexEvents.cs b/backend/src/Squidex.Domain.Apps.Events/SquidexEvents.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/SquidexEvents.cs rename to backend/src/Squidex.Domain.Apps.Events/SquidexEvents.cs diff --git a/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs b/backend/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs rename to backend/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs diff --git a/src/Squidex.Domain.Apps.Events/SquidexHeaders.cs b/backend/src/Squidex.Domain.Apps.Events/SquidexHeaders.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/SquidexHeaders.cs rename to backend/src/Squidex.Domain.Apps.Events/SquidexHeaders.cs diff --git a/src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs b/backend/src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs similarity index 100% rename from src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs rename to backend/src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs diff --git a/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs b/backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs similarity index 100% rename from src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs rename to backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs diff --git a/backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs b/backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs new file mode 100644 index 000000000..6a8bfbdd9 --- /dev/null +++ b/backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs @@ -0,0 +1,99 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Users.MongoDb +{ + public sealed class MongoUser : IdentityUser + { + public List Claims { get; set; } = new List(); + + public List Tokens { get; set; } = new List(); + + public List Logins { get; set; } = new List(); + + public HashSet Roles { get; set; } = new HashSet(); + + internal void AddLogin(UserLoginInfo login) + { + Logins.Add(new UserLoginInfo(login.LoginProvider, login.ProviderKey, login.ProviderDisplayName)); + } + + internal void AddRole(string role) + { + Roles.Add(role); + } + + internal void RemoveRole(string role) + { + Roles.Remove(role); + } + + internal void RemoveLogin(string loginProvider, string providerKey) + { + Logins.RemoveAll(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); + } + + internal void AddClaim(Claim claim) + { + Claims.Add(claim); + } + + internal void AddClaims(IEnumerable claims) + { + claims.Foreach(AddClaim); + } + + internal void RemoveClaim(Claim claim) + { + Claims.RemoveAll(c => c.Type == claim.Type && c.Value == claim.Value); + } + + internal void RemoveClaims(IEnumerable claims) + { + claims.Foreach(RemoveClaim); + } + + internal string? GetToken(string loginProvider, string name) + { + return Tokens.FirstOrDefault(t => t.LoginProvider == loginProvider && t.Name == name)?.Value; + } + + internal void AddToken(string loginProvider, string name, string value) + { + Tokens.Add(new UserTokenInfo { LoginProvider = loginProvider, Name = name, Value = value }); + } + + internal void RemoveToken(string loginProvider, string name) + { + Tokens.RemoveAll(t => t.LoginProvider == loginProvider && t.Name == name); + } + + internal void ReplaceClaim(Claim existingClaim, Claim newClaim) + { + RemoveClaim(existingClaim); + + AddClaim(newClaim); + } + + internal void SetToken(string loginProider, string name, string value) + { + RemoveToken(loginProider, name); + + AddToken(loginProider, name, value); + } + } + + public sealed class UserTokenInfo : IdentityUserToken + { + } +} diff --git a/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs b/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs new file mode 100644 index 000000000..b0900434e --- /dev/null +++ b/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs @@ -0,0 +1,526 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Users.MongoDb +{ + public sealed class MongoUserStore : + MongoRepositoryBase, + IUserAuthenticationTokenStore, + IUserAuthenticatorKeyStore, + IUserClaimStore, + IUserEmailStore, + IUserFactory, + IUserLockoutStore, + IUserLoginStore, + IUserPasswordStore, + IUserPhoneNumberStore, + IUserRoleStore, + IUserSecurityStampStore, + IUserTwoFactorStore, + IUserTwoFactorRecoveryCodeStore, + IQueryableUserStore + { + private const string InternalLoginProvider = "[AspNetUserStore]"; + private const string AuthenticatorKeyTokenName = "AuthenticatorKey"; + private const string RecoveryCodeTokenName = "RecoveryCodes"; + + static MongoUserStore() + { + BsonClassMap.RegisterClassMap(cm => + { + cm.MapConstructor(typeof(Claim).GetConstructors() + .First(x => + { + var parameters = x.GetParameters(); + + return parameters.Length == 2 && + parameters[0].Name == "type" && + parameters[0].ParameterType == typeof(string) && + parameters[1].Name == "value" && + parameters[1].ParameterType == typeof(string); + })) + .SetArguments(new[] + { + nameof(Claim.Type), + nameof(Claim.Value) + }); + + cm.MapMember(x => x.Type); + cm.MapMember(x => x.Value); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.MapConstructor(typeof(UserLoginInfo).GetConstructors().First()) + .SetArguments(new[] + { + nameof(UserLoginInfo.LoginProvider), + nameof(UserLoginInfo.ProviderKey), + nameof(UserLoginInfo.ProviderDisplayName) + }); + + cm.AutoMap(); + }); + + BsonClassMap.RegisterClassMap>(cm => + { + cm.AutoMap(); + + cm.UnmapMember(x => x.UserId); + }); + + BsonClassMap.RegisterClassMap>(cm => + { + cm.AutoMap(); + + cm.MapMember(x => x.Id).SetSerializer(new StringSerializer(BsonType.ObjectId)); + cm.MapMember(x => x.AccessFailedCount).SetIgnoreIfDefault(true); + cm.MapMember(x => x.EmailConfirmed).SetIgnoreIfDefault(true); + cm.MapMember(x => x.LockoutEnd).SetElementName("LockoutEndDateUtc").SetIgnoreIfNull(true); + cm.MapMember(x => x.LockoutEnabled).SetIgnoreIfDefault(true); + cm.MapMember(x => x.PasswordHash).SetIgnoreIfNull(true); + cm.MapMember(x => x.PhoneNumber).SetIgnoreIfNull(true); + cm.MapMember(x => x.PhoneNumberConfirmed).SetIgnoreIfDefault(true); + cm.MapMember(x => x.SecurityStamp).SetIgnoreIfNull(true); + cm.MapMember(x => x.TwoFactorEnabled).SetIgnoreIfDefault(true); + }); + } + + public MongoUserStore(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "Identity_Users"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel( + Index + .Ascending("Logins.LoginProvider") + .Ascending("Logins.ProviderKey")), + new CreateIndexModel( + Index + .Ascending(x => x.NormalizedUserName), + new CreateIndexOptions + { + Unique = true + }), + new CreateIndexModel( + Index + .Ascending(x => x.NormalizedEmail), + new CreateIndexOptions + { + Unique = true + }) + }, ct); + } + + protected override MongoCollectionSettings CollectionSettings() + { + return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; + } + + public void Dispose() + { + } + + public IQueryable Users + { + get { return Collection.AsQueryable(); } + } + + public bool IsId(string id) + { + return ObjectId.TryParse(id, out _); + } + + public IdentityUser Create(string email) + { + return new MongoUser { Email = email, UserName = email }; + } + + public async Task FindByIdAsync(string userId, CancellationToken cancellationToken) + { + if (!IsId(userId)) + { + return null!; + } + + return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken); + } + + public async Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) + { + return await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken); + } + + public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) + { + return await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken); + } + + public async Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) + { + return await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken); + } + + public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) + { + return (await Collection.Find(x => x.Claims.Any(y => y.Type == claim.Type && y.Value == claim.Value)).ToListAsync(cancellationToken)).OfType().ToList(); + } + + public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) + { + return (await Collection.Find(x => x.Roles.Contains(roleName)).ToListAsync(cancellationToken)).OfType().ToList(); + } + + public async Task CreateAsync(IdentityUser user, CancellationToken cancellationToken) + { + user.Id = ObjectId.GenerateNewId().ToString(); + + await Collection.InsertOneAsync((MongoUser)user, null, cancellationToken); + + return IdentityResult.Success; + } + + public async Task UpdateAsync(IdentityUser user, CancellationToken cancellationToken) + { + await Collection.ReplaceOneAsync(x => x.Id == user.Id, (MongoUser)user, null, cancellationToken); + + return IdentityResult.Success; + } + + public async Task DeleteAsync(IdentityUser user, CancellationToken cancellationToken) + { + await Collection.DeleteOneAsync(x => x.Id == user.Id, null, cancellationToken); + + return IdentityResult.Success; + } + + public Task GetUserIdAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).Id); + } + + public Task GetUserNameAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).UserName); + } + + public Task GetNormalizedUserNameAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).NormalizedUserName); + } + + public Task GetPasswordHashAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).PasswordHash); + } + + public Task> GetRolesAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult>(((MongoUser)user).Roles.ToList()); + } + + public Task IsInRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).Roles.Contains(roleName)); + } + + public Task> GetLoginsAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult>(((MongoUser)user).Logins.Select(x => new UserLoginInfo(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList()); + } + + public Task GetSecurityStampAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).SecurityStamp); + } + + public Task GetEmailAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).Email); + } + + public Task GetEmailConfirmedAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).EmailConfirmed); + } + + public Task GetNormalizedEmailAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).NormalizedEmail); + } + + public Task> GetClaimsAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult>(((MongoUser)user).Claims); + } + + public Task GetPhoneNumberAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).PhoneNumber); + } + + public Task GetPhoneNumberConfirmedAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).PhoneNumberConfirmed); + } + + public Task GetTwoFactorEnabledAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).TwoFactorEnabled); + } + + public Task GetLockoutEndDateAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).LockoutEnd); + } + + public Task GetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).AccessFailedCount); + } + + public Task GetLockoutEnabledAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).LockoutEnabled); + } + + public Task GetTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).GetToken(loginProvider, name)!); + } + + public Task GetAuthenticatorKeyAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).GetToken(InternalLoginProvider, AuthenticatorKeyTokenName)!); + } + + public Task HasPasswordAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(!string.IsNullOrWhiteSpace(((MongoUser)user).PasswordHash)); + } + + public Task CountCodesAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName)?.Split(';').Length ?? 0); + } + + public Task SetUserNameAsync(IdentityUser user, string userName, CancellationToken cancellationToken) + { + ((MongoUser)user).UserName = userName; + + return TaskHelper.Done; + } + + public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, CancellationToken cancellationToken) + { + ((MongoUser)user).NormalizedUserName = normalizedName; + + return TaskHelper.Done; + } + + public Task SetPasswordHashAsync(IdentityUser user, string passwordHash, CancellationToken cancellationToken) + { + ((MongoUser)user).PasswordHash = passwordHash; + + return TaskHelper.Done; + } + + public Task AddToRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) + { + ((MongoUser)user).AddRole(roleName); + + return TaskHelper.Done; + } + + public Task RemoveFromRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) + { + ((MongoUser)user).RemoveRole(roleName); + + return TaskHelper.Done; + } + + public Task AddLoginAsync(IdentityUser user, UserLoginInfo login, CancellationToken cancellationToken) + { + ((MongoUser)user).AddLogin(login); + + return TaskHelper.Done; + } + + public Task RemoveLoginAsync(IdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) + { + ((MongoUser)user).RemoveLogin(loginProvider, providerKey); + + return TaskHelper.Done; + } + + public Task SetSecurityStampAsync(IdentityUser user, string stamp, CancellationToken cancellationToken) + { + ((MongoUser)user).SecurityStamp = stamp; + + return TaskHelper.Done; + } + + public Task SetEmailAsync(IdentityUser user, string email, CancellationToken cancellationToken) + { + ((MongoUser)user).Email = email; + + return TaskHelper.Done; + } + + public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken) + { + ((MongoUser)user).EmailConfirmed = confirmed; + + return TaskHelper.Done; + } + + public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail, CancellationToken cancellationToken) + { + ((MongoUser)user).NormalizedEmail = normalizedEmail; + + return TaskHelper.Done; + } + + public Task AddClaimsAsync(IdentityUser user, IEnumerable claims, CancellationToken cancellationToken) + { + ((MongoUser)user).AddClaims(claims); + + return TaskHelper.Done; + } + + public Task ReplaceClaimAsync(IdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) + { + ((MongoUser)user).ReplaceClaim(claim, newClaim); + + return TaskHelper.Done; + } + + public Task RemoveClaimsAsync(IdentityUser user, IEnumerable claims, CancellationToken cancellationToken) + { + ((MongoUser)user).RemoveClaims(claims); + + return TaskHelper.Done; + } + + public Task SetPhoneNumberAsync(IdentityUser user, string phoneNumber, CancellationToken cancellationToken) + { + ((MongoUser)user).PhoneNumber = phoneNumber; + + return TaskHelper.Done; + } + + public Task SetPhoneNumberConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken) + { + ((MongoUser)user).PhoneNumberConfirmed = confirmed; + + return TaskHelper.Done; + } + + public Task SetTwoFactorEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken) + { + ((MongoUser)user).TwoFactorEnabled = enabled; + + return TaskHelper.Done; + } + + public Task SetLockoutEndDateAsync(IdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) + { + ((MongoUser)user).LockoutEnd = lockoutEnd?.UtcDateTime; + + return TaskHelper.Done; + } + + public Task IncrementAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) + { + ((MongoUser)user).AccessFailedCount++; + + return Task.FromResult(((MongoUser)user).AccessFailedCount); + } + + public Task ResetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) + { + ((MongoUser)user).AccessFailedCount = 0; + + return TaskHelper.Done; + } + + public Task SetLockoutEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken) + { + ((MongoUser)user).LockoutEnabled = enabled; + + return TaskHelper.Done; + } + + public Task SetTokenAsync(IdentityUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) + { + ((MongoUser)user).SetToken(loginProvider, name, value); + + return TaskHelper.Done; + } + + public Task RemoveTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + ((MongoUser)user).RemoveToken(loginProvider, name); + + return TaskHelper.Done; + } + + public Task SetAuthenticatorKeyAsync(IdentityUser user, string key, CancellationToken cancellationToken) + { + ((MongoUser)user).SetToken(InternalLoginProvider, AuthenticatorKeyTokenName, key); + + return TaskHelper.Done; + } + + public Task ReplaceCodesAsync(IdentityUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken) + { + ((MongoUser)user).SetToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", recoveryCodes)); + + return TaskHelper.Done; + } + + public Task RedeemCodeAsync(IdentityUser user, string code, CancellationToken cancellationToken) + { + var mergedCodes = ((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName) ?? string.Empty; + + var splitCodes = mergedCodes.Split(';'); + if (splitCodes.Contains(code)) + { + var updatedCodes = new List(splitCodes.Where(s => s != code)); + + ((MongoUser)user).SetToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", updatedCodes)); + + return TaskHelper.True; + } + + return TaskHelper.False; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj new file mode 100644 index 000000000..3cab46969 --- /dev/null +++ b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj @@ -0,0 +1,32 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Users/AssetUserPictureStore.cs b/backend/src/Squidex.Domain.Users/AssetUserPictureStore.cs new file mode 100644 index 000000000..48b364886 --- /dev/null +++ b/backend/src/Squidex.Domain.Users/AssetUserPictureStore.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; + +namespace Squidex.Domain.Users +{ + public sealed class AssetUserPictureStore : IUserPictureStore + { + private readonly IAssetStore assetStore; + + public AssetUserPictureStore(IAssetStore assetStore) + { + Guard.NotNull(assetStore); + + this.assetStore = assetStore; + } + + public Task UploadAsync(string userId, Stream stream) + { + return assetStore.UploadAsync(userId, 0, "picture", stream, true); + } + + public async Task DownloadAsync(string userId) + { + var memoryStream = new MemoryStream(); + + await assetStore.DownloadAsync(userId, 0, "picture", memoryStream); + + memoryStream.Position = 0; + + return memoryStream; + } + } +} diff --git a/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs b/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs new file mode 100644 index 000000000..491d0c59e --- /dev/null +++ b/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs @@ -0,0 +1,101 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Users +{ + public sealed class DefaultUserResolver : IUserResolver + { + private readonly IServiceProvider serviceProvider; + + public DefaultUserResolver(IServiceProvider serviceProvider) + { + Guard.NotNull(serviceProvider); + + this.serviceProvider = serviceProvider; + } + + public async Task CreateUserIfNotExists(string email, bool invited) + { + using (var scope = serviceProvider.CreateScope()) + { + var userFactory = scope.ServiceProvider.GetRequiredService(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + + var user = userFactory.Create(email); + + try + { + var result = await userManager.CreateAsync(user); + + if (result.Succeeded) + { + var values = new UserValues { DisplayName = email, Invited = invited }; + + await userManager.UpdateAsync(user, values); + } + + return result.Succeeded; + } + catch + { + return false; + } + } + } + + public async Task FindByIdOrEmailAsync(string idOrEmail) + { + using (var scope = serviceProvider.CreateScope()) + { + var userFactory = scope.ServiceProvider.GetRequiredService(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + + if (userFactory.IsId(idOrEmail)) + { + return await userManager.FindByIdWithClaimsAsync(idOrEmail); + } + else + { + return await userManager.FindByEmailWithClaimsAsyncAsync(idOrEmail); + } + } + } + + public async Task> QueryByEmailAsync(string email) + { + using (var scope = serviceProvider.CreateScope()) + { + var userManager = scope.ServiceProvider.GetRequiredService>(); + + var result = await userManager.QueryByEmailAsync(email); + + return result.OfType().ToList(); + } + } + + public async Task> QueryManyAsync(string[] ids) + { + using (var scope = serviceProvider.CreateScope()) + { + var userManager = scope.ServiceProvider.GetRequiredService>(); + + var result = await userManager.QueryByIdsAync(ids); + + return result.OfType().ToDictionary(x => x.Id); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs b/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs new file mode 100644 index 000000000..621d45153 --- /dev/null +++ b/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Xml.Linq; +using Microsoft.AspNetCore.DataProtection.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Users +{ + public sealed class DefaultXmlRepository : IXmlRepository + { + private readonly ISnapshotStore store; + + [CollectionName("XmlRepository")] + public sealed class State + { + public string Xml { get; set; } + } + + public DefaultXmlRepository(ISnapshotStore store) + { + Guard.NotNull(store); + + this.store = store; + } + + public IReadOnlyCollection GetAllElements() + { + var result = new List(); + + store.ReadAllAsync((state, version) => + { + result.Add(XElement.Parse(state.Xml)); + + return TaskHelper.Done; + }).Wait(); + + return result; + } + + public void StoreElement(XElement element, string friendlyName) + { + store.WriteAsync(friendlyName, new State { Xml = element.ToString() }, EtagVersion.Any, EtagVersion.Any).Wait(); + } + } +} diff --git a/src/Squidex.Domain.Users/IUserEvents.cs b/backend/src/Squidex.Domain.Users/IUserEvents.cs similarity index 100% rename from src/Squidex.Domain.Users/IUserEvents.cs rename to backend/src/Squidex.Domain.Users/IUserEvents.cs diff --git a/src/Squidex.Domain.Users/IUserFactory.cs b/backend/src/Squidex.Domain.Users/IUserFactory.cs similarity index 100% rename from src/Squidex.Domain.Users/IUserFactory.cs rename to backend/src/Squidex.Domain.Users/IUserFactory.cs diff --git a/src/Squidex.Domain.Users/IUserPictureStore.cs b/backend/src/Squidex.Domain.Users/IUserPictureStore.cs similarity index 100% rename from src/Squidex.Domain.Users/IUserPictureStore.cs rename to backend/src/Squidex.Domain.Users/IUserPictureStore.cs diff --git a/src/Squidex.Domain.Users/NoopUserEvents.cs b/backend/src/Squidex.Domain.Users/NoopUserEvents.cs similarity index 100% rename from src/Squidex.Domain.Users/NoopUserEvents.cs rename to backend/src/Squidex.Domain.Users/NoopUserEvents.cs diff --git a/backend/src/Squidex.Domain.Users/PwnedPasswordValidator.cs b/backend/src/Squidex.Domain.Users/PwnedPasswordValidator.cs new file mode 100644 index 000000000..accc35925 --- /dev/null +++ b/backend/src/Squidex.Domain.Users/PwnedPasswordValidator.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using SharpPwned.NET; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Users +{ + public sealed class PwnedPasswordValidator : IPasswordValidator + { + private const string ErrorCode = "PwnedError"; + private const string ErrorText = "This password has previously appeared in a data breach and should never be used. If you've ever used it anywhere before, change it!"; + private static readonly IdentityResult Error = IdentityResult.Failed(new IdentityError { Code = ErrorCode, Description = ErrorText }); + + private readonly HaveIBeenPwnedRestClient client = new HaveIBeenPwnedRestClient(); + private readonly ISemanticLog log; + + public PwnedPasswordValidator(ISemanticLog log) + { + Guard.NotNull(log); + + this.log = log; + } + + public async Task ValidateAsync(UserManager manager, IdentityUser user, string password) + { + try + { + var isBreached = await client.IsPasswordPwned(password); + + if (isBreached) + { + return Error; + } + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("operation", "CheckPasswordPwned") + .WriteProperty("status", "Failed")); + } + + return IdentityResult.Success; + } + } +} diff --git a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj new file mode 100644 index 000000000..617da32a7 --- /dev/null +++ b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj @@ -0,0 +1,31 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs b/backend/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs similarity index 100% rename from src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs rename to backend/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs diff --git a/backend/src/Squidex.Domain.Users/UserManagerExtensions.cs b/backend/src/Squidex.Domain.Users/UserManagerExtensions.cs new file mode 100644 index 000000000..251aa1420 --- /dev/null +++ b/backend/src/Squidex.Domain.Users/UserManagerExtensions.cs @@ -0,0 +1,286 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Squidex.Shared.Identity; + +namespace Squidex.Domain.Users +{ + public static class UserManagerExtensions + { + public static async Task GetUserWithClaimsAsync(this UserManager userManager, ClaimsPrincipal principal) + { + if (principal == null) + { + return null; + } + + var user = await userManager.FindByIdWithClaimsAsync(userManager.GetUserId(principal)); + + return user; + } + + public static async Task ResolveUserAsync(this UserManager userManager, IdentityUser user) + { + if (user == null) + { + return null; + } + + var claims = await userManager.GetClaimsAsync(user); + + return new UserWithClaims(user, claims); + } + + public static async Task FindByIdWithClaimsAsync(this UserManager userManager, string id) + { + if (id == null) + { + return null; + } + + var user = await userManager.FindByIdAsync(id); + + return await userManager.ResolveUserAsync(user); + } + + public static async Task FindByEmailWithClaimsAsyncAsync(this UserManager userManager, string email) + { + if (email == null) + { + return null; + } + + var user = await userManager.FindByEmailAsync(email); + + return await userManager.ResolveUserAsync(user); + } + + public static async Task FindByLoginWithClaimsAsync(this UserManager userManager, string loginProvider, string providerKey) + { + if (loginProvider == null || providerKey == null) + { + return null; + } + + var user = await userManager.FindByLoginAsync(loginProvider, providerKey); + + return await userManager.ResolveUserAsync(user); + } + + public static Task CountByEmailAsync(this UserManager userManager, string? email = null) + { + var count = QueryUsers(userManager, email).LongCount(); + + return Task.FromResult(count); + } + + public static async Task> QueryByIdsAync(this UserManager userManager, string[] ids) + { + var users = userManager.Users.Where(x => ids.Contains(x.Id)).ToList(); + + var result = await userManager.ResolveUsersAsync(users); + + return result.ToList(); + } + + public static async Task> QueryByEmailAsync(this UserManager userManager, string? email = null, int take = 10, int skip = 0) + { + var users = QueryUsers(userManager, email).Skip(skip).Take(take).ToList(); + + var result = await userManager.ResolveUsersAsync(users); + + return result.ToList(); + } + + public static Task ResolveUsersAsync(this UserManager userManager, IEnumerable users) + { + return Task.WhenAll(users.Select(async user => + { + return (await userManager.ResolveUserAsync(user))!; + })); + } + + public static IQueryable QueryUsers(UserManager userManager, string? email = null) + { + var result = userManager.Users; + + if (!string.IsNullOrWhiteSpace(email)) + { + var normalizedEmail = userManager.NormalizeEmail(email); + + result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail)); + } + + return result; + } + + public static async Task CreateAsync(this UserManager userManager, IUserFactory factory, UserValues values) + { + var user = factory.Create(values.Email); + + try + { + await DoChecked(() => userManager.CreateAsync(user), "Cannot create user."); + + var claims = values.ToClaims(true); + + if (claims.Count > 0) + { + await DoChecked(() => userManager.AddClaimsAsync(user, claims), "Cannot add user."); + } + + if (!string.IsNullOrWhiteSpace(values.Password)) + { + await DoChecked(() => userManager.AddPasswordAsync(user, values.Password), "Cannot create user."); + } + } + catch + { + await userManager.DeleteAsync(user); + + throw; + } + + return (await userManager.ResolveUserAsync(user))!; + } + + public static async Task UpdateAsync(this UserManager userManager, string id, UserValues values) + { + var user = await userManager.FindByIdAsync(id); + + if (user == null) + { + throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); + } + + await UpdateAsync(userManager, user, values); + + return (await userManager.ResolveUserAsync(user))!; + } + + public static Task GenerateClientSecretAsync(this UserManager userManager, IdentityUser user) + { + var claims = new List { new Claim(SquidexClaimTypes.ClientSecret, RandomHash.New()) }; + + return userManager.SyncClaimsAsync(user, claims); + } + + public static async Task UpdateSafeAsync(this UserManager userManager, IdentityUser user, UserValues values) + { + try + { + await userManager.UpdateAsync(user, values); + + return IdentityResult.Success; + } + catch (ValidationException ex) + { + return IdentityResult.Failed(ex.Errors.Select(x => new IdentityError { Description = x.Message }).ToArray()); + } + } + + public static async Task UpdateAsync(this UserManager userManager, IdentityUser user, UserValues values) + { + if (user == null) + { + throw new DomainObjectNotFoundException("Id", typeof(IdentityUser)); + } + + if (!string.IsNullOrWhiteSpace(values.Email) && values.Email != user.Email) + { + await DoChecked(() => userManager.SetEmailAsync(user, values.Email), "Cannot update email."); + await DoChecked(() => userManager.SetUserNameAsync(user, values.Email), "Cannot update email."); + } + + await DoChecked(() => userManager.SyncClaimsAsync(user, values.ToClaims(false)), "Cannot update user."); + + if (!string.IsNullOrWhiteSpace(values.Password)) + { + await DoChecked(() => userManager.RemovePasswordAsync(user), "Cannot replace password."); + await DoChecked(() => userManager.AddPasswordAsync(user, values.Password), "Cannot replace password."); + } + } + + public static async Task LockAsync(this UserManager userManager, string id) + { + var user = await userManager.FindByIdAsync(id); + + if (user == null) + { + throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); + } + + await DoChecked(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100)), "Cannot lock user."); + + return (await userManager.ResolveUserAsync(user))!; + } + + public static async Task UnlockAsync(this UserManager userManager, string id) + { + var user = await userManager.FindByIdAsync(id); + + if (user == null) + { + throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); + } + + await DoChecked(() => userManager.SetLockoutEndDateAsync(user, null), "Cannot unlock user."); + + return (await userManager.ResolveUserAsync(user))!; + } + + private static async Task DoChecked(Func> action, string message) + { + var result = await action(); + + if (!result.Succeeded) + { + throw new ValidationException(message, result.Errors.Select(x => new ValidationError(x.Description)).ToArray()); + } + } + + public static async Task SyncClaimsAsync(this UserManager userManager, IdentityUser user, List claims) + { + if (claims.Any()) + { + var oldClaims = await userManager.GetClaimsAsync(user); + + var oldClaimsToRemove = new List(); + + foreach (var oldClaim in oldClaims) + { + if (claims.Any(x => x.Type == oldClaim.Type)) + { + oldClaimsToRemove.Add(oldClaim); + } + } + + if (oldClaimsToRemove.Count > 0) + { + var result = await userManager.RemoveClaimsAsync(user, oldClaimsToRemove); + + if (!result.Succeeded) + { + return result; + } + } + + return await userManager.AddClaimsAsync(user, claims.Where(x => !string.IsNullOrWhiteSpace(x.Value))); + } + + return IdentityResult.Success; + } + } +} diff --git a/src/Squidex.Domain.Users/UserValues.cs b/backend/src/Squidex.Domain.Users/UserValues.cs similarity index 100% rename from src/Squidex.Domain.Users/UserValues.cs rename to backend/src/Squidex.Domain.Users/UserValues.cs diff --git a/backend/src/Squidex.Domain.Users/UserWithClaims.cs b/backend/src/Squidex.Domain.Users/UserWithClaims.cs new file mode 100644 index 000000000..17f6f6bad --- /dev/null +++ b/backend/src/Squidex.Domain.Users/UserWithClaims.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Users +{ + public sealed class UserWithClaims : IUser + { + public IdentityUser Identity { get; } + + public List Claims { get; } + + public string Id + { + get { return Identity.Id; } + } + + public string Email + { + get { return Identity.Email; } + } + + public bool IsLocked + { + get { return Identity.LockoutEnd > DateTime.Now.ToUniversalTime(); } + } + + IReadOnlyList IUser.Claims + { + get { return Claims; } + } + + public UserWithClaims(IdentityUser user, IEnumerable claims) + { + Guard.NotNull(user); + Guard.NotNull(claims); + + Identity = user; + + Claims = claims.ToList(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs b/backend/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs new file mode 100644 index 000000000..58cdb0db9 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.WindowsAzure.Storage; +using Microsoft.WindowsAzure.Storage.Blob; + +namespace Squidex.Infrastructure.Assets +{ + public class AzureBlobAssetStore : IAssetStore, IInitializable + { + private readonly string containerName; + private readonly string connectionString; + private CloudBlobContainer blobContainer; + + public AzureBlobAssetStore(string connectionString, string containerName) + { + Guard.NotNullOrEmpty(containerName); + Guard.NotNullOrEmpty(connectionString); + + this.connectionString = connectionString; + this.containerName = containerName; + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + try + { + var storageAccount = CloudStorageAccount.Parse(connectionString); + + var blobClient = storageAccount.CreateCloudBlobClient(); + var blobReference = blobClient.GetContainerReference(containerName); + + await blobReference.CreateIfNotExistsAsync(); + + blobContainer = blobReference; + } + catch (Exception ex) + { + throw new ConfigurationException($"Cannot connect to blob container '{containerName}'.", ex); + } + } + + public string? GeneratePublicUrl(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + if (blobContainer.Properties.PublicAccess != BlobContainerPublicAccessType.Blob) + { + var blob = blobContainer.GetBlockBlobReference(fileName); + + return blob.Uri.ToString(); + } + + return null; + } + + public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(sourceFileName); + Guard.NotNullOrEmpty(targetFileName); + + try + { + var sourceBlob = blobContainer.GetBlockBlobReference(sourceFileName); + + var targetBlob = blobContainer.GetBlobReference(targetFileName); + + await targetBlob.StartCopyAsync(sourceBlob.Uri, null, AccessCondition.GenerateIfNotExistsCondition(), null, null, ct); + + while (targetBlob.CopyState.Status == CopyStatus.Pending) + { + ct.ThrowIfCancellationRequested(); + + await Task.Delay(50, ct); + await targetBlob.FetchAttributesAsync(null, null, null, ct); + } + + if (targetBlob.CopyState.Status != CopyStatus.Success) + { + throw new StorageException($"Copy of temporary file failed: {targetBlob.CopyState.Status}"); + } + } + catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 409) + { + throw new AssetAlreadyExistsException(targetFileName); + } + catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404) + { + throw new AssetNotFoundException(sourceFileName, ex); + } + } + + public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + + try + { + var blob = blobContainer.GetBlockBlobReference(fileName); + + await blob.DownloadToStreamAsync(stream, null, null, null, ct); + } + catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404) + { + throw new AssetNotFoundException(fileName, ex); + } + } + + public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + + try + { + var tempBlob = blobContainer.GetBlockBlobReference(fileName); + + await tempBlob.UploadFromStreamAsync(stream, overwrite ? null : AccessCondition.GenerateIfNotExistsCondition(), null, null, ct); + } + catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 409) + { + throw new AssetAlreadyExistsException(fileName); + } + } + + public Task DeleteAsync(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + var blob = blobContainer.GetBlockBlobReference(fileName); + + return blob.DeleteIfExistsAsync(); + } + } +} diff --git a/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs b/backend/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs similarity index 100% rename from src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs rename to backend/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs similarity index 100% rename from src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs rename to backend/src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs similarity index 100% rename from src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs rename to backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs similarity index 100% rename from src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs rename to backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs new file mode 100644 index 000000000..d40d3b7d5 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs @@ -0,0 +1,139 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.ObjectModel; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents.Client; +using Newtonsoft.Json; +using Index = Microsoft.Azure.Documents.Index; + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed partial class CosmosDbEventStore : DisposableObjectBase, IEventStore, IInitializable + { + private readonly DocumentClient documentClient; + private readonly Uri collectionUri; + private readonly Uri databaseUri; + private readonly string masterKey; + private readonly string databaseId; + private readonly JsonSerializerSettings serializerSettings; + + public JsonSerializerSettings SerializerSettings + { + get { return serializerSettings; } + } + + public string DatabaseId + { + get { return databaseId; } + } + + public string MasterKey + { + get { return masterKey; } + } + + public Uri ServiceUri + { + get { return documentClient.ServiceEndpoint; } + } + + public CosmosDbEventStore(DocumentClient documentClient, string masterKey, string database, JsonSerializerSettings serializerSettings) + { + Guard.NotNull(documentClient); + Guard.NotNull(serializerSettings); + Guard.NotNullOrEmpty(masterKey); + Guard.NotNullOrEmpty(database); + + this.documentClient = documentClient; + + databaseUri = UriFactory.CreateDatabaseUri(database); + databaseId = database; + + collectionUri = UriFactory.CreateDocumentCollectionUri(database, Constants.Collection); + + this.masterKey = masterKey; + + this.serializerSettings = serializerSettings; + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + documentClient.Dispose(); + } + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + await documentClient.CreateDatabaseIfNotExistsAsync(new Database { Id = databaseId }); + + await documentClient.CreateDocumentCollectionIfNotExistsAsync(databaseUri, + new DocumentCollection + { + PartitionKey = new PartitionKeyDefinition + { + Paths = new Collection + { + "/PartitionId" + } + }, + Id = Constants.LeaseCollection + }); + + await documentClient.CreateDocumentCollectionIfNotExistsAsync(databaseUri, + new DocumentCollection + { + PartitionKey = new PartitionKeyDefinition + { + Paths = new Collection + { + "/eventStream" + } + }, + IndexingPolicy = new IndexingPolicy + { + IncludedPaths = new Collection + { + new IncludedPath + { + Path = "/*", + Indexes = new Collection + { + Index.Range(DataType.Number), + Index.Range(DataType.String) + } + } + } + }, + UniqueKeyPolicy = new UniqueKeyPolicy + { + UniqueKeys = new Collection + { + new UniqueKey + { + Paths = new Collection + { + "/eventStream", + "/eventStreamOffset" + } + } + } + }, + Id = Constants.Collection + }, + new RequestOptions + { + PartitionKey = new PartitionKey("/eventStream") + }); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs new file mode 100644 index 000000000..c0451bff2 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.EventSourcing +{ + public delegate bool EventPredicate(EventData data); + + public partial class CosmosDbEventStore : IEventStore, IInitializable + { + public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string? streamFilter = null, string? position = null) + { + Guard.NotNull(subscriber); + + ThrowIfDisposed(); + + return new CosmosDbSubscription(this, subscriber, streamFilter, position); + } + + public Task CreateIndexAsync(string property) + { + Guard.NotNullOrEmpty(property); + + ThrowIfDisposed(); + + return TaskHelper.Done; + } + + public async Task> QueryAsync(string streamName, long streamPosition = 0) + { + Guard.NotNullOrEmpty(streamName); + + ThrowIfDisposed(); + + using (Profiler.TraceMethod()) + { + var query = FilterBuilder.ByStreamName(streamName, streamPosition - MaxCommitSize); + + var result = new List(); + + await documentClient.QueryAsync(collectionUri, query, commit => + { + var eventStreamOffset = (int)commit.EventStreamOffset; + + var commitTimestamp = commit.Timestamp; + var commitOffset = 0; + + foreach (var @event in commit.Events) + { + eventStreamOffset++; + + if (eventStreamOffset >= streamPosition) + { + var eventData = @event.ToEventData(); + var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + + result.Add(new StoredEvent(streamName, eventToken, eventStreamOffset, eventData)); + } + } + + return TaskHelper.Done; + }); + + return result; + } + } + + public Task QueryAsync(Func callback, string property, object value, string? position = null, CancellationToken ct = default) + { + Guard.NotNull(callback); + Guard.NotNullOrEmpty(property); + Guard.NotNull(value); + + ThrowIfDisposed(); + + StreamPosition lastPosition = position; + + var filterDefinition = FilterBuilder.CreateByProperty(property, value, lastPosition); + var filterExpression = FilterBuilder.CreateExpression(property, value); + + return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); + } + + public Task QueryAsync(Func callback, string? streamFilter = null, string? position = null, CancellationToken ct = default) + { + Guard.NotNull(callback); + + ThrowIfDisposed(); + + StreamPosition lastPosition = position; + + var filterDefinition = FilterBuilder.CreateByFilter(streamFilter, lastPosition); + var filterExpression = FilterBuilder.CreateExpression(null, null); + + return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); + } + + private async Task QueryAsync(Func callback, StreamPosition lastPosition, SqlQuerySpec query, EventPredicate filterExpression, CancellationToken ct = default) + { + using (Profiler.TraceMethod()) + { + await documentClient.QueryAsync(collectionUri, query, async commit => + { + var eventStreamOffset = (int)commit.EventStreamOffset; + + var commitTimestamp = commit.Timestamp; + var commitOffset = 0; + + foreach (var @event in commit.Events) + { + eventStreamOffset++; + + if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) + { + var eventData = @event.ToEventData(); + + if (filterExpression(eventData)) + { + var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + + await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData)); + } + } + + commitOffset++; + } + }, ct); + } + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs new file mode 100644 index 000000000..52e848345 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs @@ -0,0 +1,149 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents.Client; +using NodaTime; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.EventSourcing +{ + public partial class CosmosDbEventStore + { + private const int MaxWriteAttempts = 20; + private const int MaxCommitSize = 10; + + public Task DeleteStreamAsync(string streamName) + { + Guard.NotNullOrEmpty(streamName); + + ThrowIfDisposed(); + + var query = FilterBuilder.AllIds(streamName); + + return documentClient.QueryAsync(collectionUri, query, commit => + { + var documentUri = UriFactory.CreateDocumentUri(databaseId, Constants.Collection, commit.Id.ToString()); + + return documentClient.DeleteDocumentAsync(documentUri); + }); + } + + public Task AppendAsync(Guid commitId, string streamName, ICollection events) + { + return AppendAsync(commitId, streamName, EtagVersion.Any, events); + } + + public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + Guard.NotEmpty(commitId); + Guard.NotNullOrEmpty(streamName); + Guard.NotNull(events); + Guard.LessThan(events.Count, MaxCommitSize, "events.Count"); + + ThrowIfDisposed(); + + using (Profiler.TraceMethod()) + { + if (events.Count == 0) + { + return; + } + + var currentVersion = await GetEventStreamOffsetAsync(streamName); + + if (expectedVersion > EtagVersion.Any && expectedVersion != currentVersion) + { + throw new WrongEventVersionException(currentVersion, expectedVersion); + } + + var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events); + + for (var attempt = 0; attempt < MaxWriteAttempts; attempt++) + { + try + { + await documentClient.CreateDocumentAsync(collectionUri, commit); + + return; + } + catch (DocumentClientException ex) + { + if (ex.StatusCode == HttpStatusCode.Conflict) + { + currentVersion = await GetEventStreamOffsetAsync(streamName); + + if (expectedVersion > EtagVersion.Any) + { + throw new WrongEventVersionException(currentVersion, expectedVersion); + } + + if (attempt < MaxWriteAttempts) + { + expectedVersion = currentVersion; + } + else + { + throw new TimeoutException("Could not acquire a free slot for the commit within the provided time."); + } + } + else + { + throw; + } + } + } + } + } + + private async Task GetEventStreamOffsetAsync(string streamName) + { + var query = + documentClient.CreateDocumentQuery(collectionUri, + FilterBuilder.LastPosition(streamName)); + + var document = await query.FirstOrDefaultAsync(); + + if (document != null) + { + return document.EventStreamOffset + document.EventsCount; + } + + return EtagVersion.Empty; + } + + private static CosmosDbEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + var commitEvents = new CosmosDbEvent[events.Count]; + + var i = 0; + + foreach (var e in events) + { + var mongoEvent = CosmosDbEvent.FromEventData(e); + + commitEvents[i++] = mongoEvent; + } + + var mongoCommit = new CosmosDbEventCommit + { + Id = commitId, + Events = commitEvents, + EventsCount = events.Count, + EventStream = streamName, + EventStreamOffset = expectedVersion, + Timestamp = SystemClock.Instance.GetCurrentInstant().ToUnixTimeTicks() + }; + + return mongoCommit; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs new file mode 100644 index 000000000..65b0d0965 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs @@ -0,0 +1,151 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents.ChangeFeedProcessor.FeedProcessing; +using Newtonsoft.Json; +using Squidex.Infrastructure.Tasks; +using Builder = Microsoft.Azure.Documents.ChangeFeedProcessor.ChangeFeedProcessorBuilder; +using Collection = Microsoft.Azure.Documents.ChangeFeedProcessor.DocumentCollectionInfo; +using Options = Microsoft.Azure.Documents.ChangeFeedProcessor.ChangeFeedProcessorOptions; + +#pragma warning disable IDE0017 // Simplify object initialization + +namespace Squidex.Infrastructure.EventSourcing +{ + internal sealed class CosmosDbSubscription : IEventSubscription, IChangeFeedObserverFactory, IChangeFeedObserver + { + private readonly TaskCompletionSource processorStopRequested = new TaskCompletionSource(); + private readonly Task processorTask; + private readonly CosmosDbEventStore store; + private readonly Regex regex; + private readonly string? hostName; + private readonly IEventSubscriber subscriber; + + public CosmosDbSubscription(CosmosDbEventStore store, IEventSubscriber subscriber, string? streamFilter, string? position = null) + { + this.store = store; + + var fromBeginning = string.IsNullOrWhiteSpace(position); + + if (fromBeginning) + { + hostName = $"squidex.{DateTime.UtcNow.Ticks.ToString()}"; + } + else + { + hostName = position; + } + + if (!StreamFilter.IsAll(streamFilter)) + { + regex = new Regex(streamFilter); + } + + this.subscriber = subscriber; + + processorTask = Task.Run(async () => + { + try + { + Collection CreateCollection(string name) + { + var collection = new Collection(); + + collection.CollectionName = name; + collection.DatabaseName = store.DatabaseId; + collection.MasterKey = store.MasterKey; + collection.Uri = store.ServiceUri; + + return collection; + } + + var processor = + await new Builder() + .WithFeedCollection(CreateCollection(Constants.Collection)) + .WithLeaseCollection(CreateCollection(Constants.LeaseCollection)) + .WithHostName(hostName) + .WithProcessorOptions(new Options { StartFromBeginning = fromBeginning, LeasePrefix = hostName }) + .WithObserverFactory(this) + .BuildAsync(); + + await processor.StartAsync(); + await processorStopRequested.Task; + await processor.StopAsync(); + } + catch (Exception ex) + { + await subscriber.OnErrorAsync(this, ex); + } + }); + } + + public IChangeFeedObserver CreateObserver() + { + return this; + } + + public async Task CloseAsync(IChangeFeedObserverContext context, ChangeFeedObserverCloseReason reason) + { + if (reason == ChangeFeedObserverCloseReason.ObserverError) + { + await subscriber.OnErrorAsync(this, new InvalidOperationException("Change feed observer failed.")); + } + } + + public Task OpenAsync(IChangeFeedObserverContext context) + { + return TaskHelper.Done; + } + + public async Task ProcessChangesAsync(IChangeFeedObserverContext context, IReadOnlyList docs, CancellationToken cancellationToken) + { + if (!processorStopRequested.Task.IsCompleted) + { + foreach (var document in docs) + { + if (!processorStopRequested.Task.IsCompleted) + { + var streamName = document.GetPropertyValue("eventStream"); + + if (regex == null || regex.IsMatch(streamName)) + { + var commit = JsonConvert.DeserializeObject(document.ToString(), store.SerializerSettings); + + var eventStreamOffset = (int)commit.EventStreamOffset; + + foreach (var @event in commit.Events) + { + eventStreamOffset++; + + var eventData = @event.ToEventData(); + + await subscriber.OnEventAsync(this, new StoredEvent(commit.EventStream, hostName ?? "None", eventStreamOffset, eventData)); + } + } + } + } + } + } + + public void WakeUp() + { + } + + public Task StopAsync() + { + processorStopRequested.SetResult(true); + + return processorTask; + } + } +} diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs new file mode 100644 index 000000000..301be5b47 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs @@ -0,0 +1,156 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Microsoft.Azure.Documents; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Infrastructure.EventSourcing +{ + internal static class FilterBuilder + { + public static SqlQuerySpec AllIds(string streamName) + { + var query = + $"SELECT TOP 1 " + + $" e.id," + + $" e.eventsCount " + + $"FROM {Constants.Collection} e " + + $"WHERE " + + $" e.eventStream = @name " + + $"ORDER BY e.eventStreamOffset DESC"; + + var parameters = new SqlParameterCollection + { + new SqlParameter("@name", streamName) + }; + + return new SqlQuerySpec(query, parameters); + } + + public static SqlQuerySpec LastPosition(string streamName) + { + var query = + $"SELECT TOP 1 " + + $" e.eventStreamOffset," + + $" e.eventsCount " + + $"FROM {Constants.Collection} e " + + $"WHERE " + + $" e.eventStream = @name " + + $"ORDER BY e.eventStreamOffset DESC"; + + var parameters = new SqlParameterCollection + { + new SqlParameter("@name", streamName) + }; + + return new SqlQuerySpec(query, parameters); + } + + public static SqlQuerySpec ByStreamName(string streamName, long streamPosition = 0) + { + var query = + $"SELECT * " + + $"FROM {Constants.Collection} e " + + $"WHERE " + + $" e.eventStream = @name " + + $"AND e.eventStreamOffset >= @position " + + $"ORDER BY e.eventStreamOffset ASC"; + + var parameters = new SqlParameterCollection + { + new SqlParameter("@name", streamName), + new SqlParameter("@position", streamPosition) + }; + + return new SqlQuerySpec(query, parameters); + } + + public static SqlQuerySpec CreateByProperty(string property, object value, StreamPosition streamPosition) + { + var filters = new List(); + + var parameters = new SqlParameterCollection(); + + filters.ForPosition(parameters, streamPosition); + filters.ForProperty(parameters, property, value); + + return BuildQuery(filters, parameters); + } + + public static SqlQuerySpec CreateByFilter(string? streamFilter, StreamPosition streamPosition) + { + var filters = new List(); + + var parameters = new SqlParameterCollection(); + + filters.ForPosition(parameters, streamPosition); + filters.ForRegex(parameters, streamFilter); + + return BuildQuery(filters, parameters); + } + + private static SqlQuerySpec BuildQuery(IEnumerable filters, SqlParameterCollection parameters) + { + var query = $"SELECT * FROM {Constants.Collection} e WHERE {string.Join(" AND ", filters)} ORDER BY e.timestamp"; + + return new SqlQuerySpec(query, parameters); + } + + private static void ForProperty(this ICollection filters, SqlParameterCollection parameters, string property, object value) + { + filters.Add($"ARRAY_CONTAINS(e.events, {{ \"header\": {{ \"{property}\": @value }} }}, true)"); + + parameters.Add(new SqlParameter("@value", value)); + } + + private static void ForRegex(this ICollection filters, SqlParameterCollection parameters, string? streamFilter) + { + if (!StreamFilter.IsAll(streamFilter)) + { + if (streamFilter.Contains("^")) + { + filters.Add($"STARTSWITH(e.eventStream, @filter)"); + } + else + { + filters.Add($"e.eventStream = @filter"); + } + + parameters.Add(new SqlParameter("@filter", streamFilter)); + } + } + + private static void ForPosition(this ICollection filters, SqlParameterCollection parameters, StreamPosition streamPosition) + { + if (streamPosition.IsEndOfCommit) + { + filters.Add($"e.timestamp > @time"); + } + else + { + filters.Add($"e.timestamp >= @time"); + } + + parameters.Add(new SqlParameter("@time", streamPosition.Timestamp)); + } + + public static EventPredicate CreateExpression(string? property, object? value) + { + if (!string.IsNullOrWhiteSpace(property)) + { + var jsonValue = JsonValue.Create(value); + + return x => x.Headers.TryGetValue(property, out var p) && p.Equals(jsonValue); + } + else + { + return x => true; + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs new file mode 100644 index 000000000..77dc23c86 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents.Client; +using Microsoft.Azure.Documents.Linq; + +namespace Squidex.Infrastructure.EventSourcing +{ + internal static class FilterExtensions + { + public static async Task FirstOrDefaultAsync(this IQueryable queryable, CancellationToken ct = default) + { + var documentQuery = queryable.AsDocumentQuery(); + + using (documentQuery) + { + if (documentQuery.HasMoreResults) + { + var results = await documentQuery.ExecuteNextAsync(ct); + + return results.FirstOrDefault(); + } + } + + return default!; + } + + public static Task QueryAsync(this DocumentClient documentClient, Uri collectionUri, SqlQuerySpec querySpec, Func handler, CancellationToken ct = default) + { + var query = documentClient.CreateDocumentQuery(collectionUri, querySpec); + + return query.QueryAsync(handler, ct); + } + + public static async Task QueryAsync(this IQueryable queryable, Func handler, CancellationToken ct = default) + { + var documentQuery = queryable.AsDocumentQuery(); + + using (documentQuery) + { + while (documentQuery.HasMoreResults && !ct.IsCancellationRequested) + { + var items = await documentQuery.ExecuteNextAsync(ct); + + foreach (var item in items) + { + await handler(item); + } + } + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs new file mode 100644 index 000000000..d783f0c41 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.EventSourcing +{ + internal sealed class StreamPosition + { + public long Timestamp { get; } + + public long CommitOffset { get; } + + public long CommitSize { get; } + + public bool IsEndOfCommit + { + get { return CommitOffset == CommitSize - 1; } + } + + public StreamPosition(long timestamp, long commitOffset, long commitSize) + { + Timestamp = timestamp; + + CommitOffset = commitOffset; + CommitSize = commitSize; + } + + public static implicit operator string(StreamPosition position) + { + var parts = new object[] + { + position.Timestamp, + position.CommitOffset, + position.CommitSize + }; + + return string.Join("-", parts); + } + + public static implicit operator StreamPosition(string? position) + { + if (!string.IsNullOrWhiteSpace(position)) + { + var parts = position.Split('-'); + + return new StreamPosition(long.Parse(parts[0]), long.Parse(parts[1]), long.Parse(parts[2])); + } + + return new StreamPosition(0, -1, -1); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj b/backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj new file mode 100644 index 000000000..00953e0bb --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj @@ -0,0 +1,24 @@ + + + netcoreapp3.0 + Squidex.Infrastructure + 8.0 + enable + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs b/backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs new file mode 100644 index 000000000..983c3b83b --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using EventStore.ClientAPI; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class GetEventStoreHealthCheck : IHealthCheck + { + private readonly IEventStoreConnection connection; + + public GetEventStoreHealthCheck(IEventStoreConnection connection) + { + Guard.NotNull(connection); + + this.connection = connection; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + await connection.ReadEventAsync("test", 1, false); + + return HealthCheckResult.Healthy("Application must query data from EventStore."); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs new file mode 100644 index 000000000..99dfe137b --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using EventStore.ClientAPI; +using Squidex.Infrastructure.Json; +using EventStoreData = EventStore.ClientAPI.EventData; + +namespace Squidex.Infrastructure.EventSourcing +{ + public static class Formatter + { + private static readonly HashSet PrivateHeaders = new HashSet { "$v", "$p", "$c", "$causedBy" }; + + public static StoredEvent Read(ResolvedEvent resolvedEvent, string? prefix, IJsonSerializer serializer) + { + var @event = resolvedEvent.Event; + + var eventPayload = Encoding.UTF8.GetString(@event.Data); + var eventHeaders = GetHeaders(serializer, @event); + + var eventData = new EventData(@event.EventType, eventHeaders, eventPayload); + + var streamName = GetStreamName(prefix, @event); + + return new StoredEvent( + streamName, + resolvedEvent.OriginalEventNumber.ToString(), + resolvedEvent.Event.EventNumber, + eventData); + } + + private static string GetStreamName(string? prefix, RecordedEvent @event) + { + var streamName = @event.EventStreamId; + + if (prefix != null && streamName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + streamName = streamName.Substring(prefix.Length + 1); + } + + return streamName; + } + + private static EnvelopeHeaders GetHeaders(IJsonSerializer serializer, RecordedEvent @event) + { + var headersJson = Encoding.UTF8.GetString(@event.Metadata); + var headers = serializer.Deserialize(headersJson); + + foreach (var key in headers.Keys.ToList()) + { + if (PrivateHeaders.Contains(key)) + { + headers.Remove(key); + } + } + + return headers; + } + + public static EventStoreData Write(EventData eventData, IJsonSerializer serializer) + { + var payload = Encoding.UTF8.GetBytes(eventData.Payload); + + var headersJson = serializer.Serialize(eventData.Headers); + var headersBytes = Encoding.UTF8.GetBytes(headersJson); + + return new EventStoreData(Guid.NewGuid(), eventData.Type, true, payload, headersBytes); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs new file mode 100644 index 000000000..f9525cb6a --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs @@ -0,0 +1,224 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventStore.ClientAPI; +using EventStore.ClientAPI.Exceptions; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class GetEventStore : IEventStore, IInitializable + { + private const int WritePageSize = 500; + private const int ReadPageSize = 500; + private readonly IEventStoreConnection connection; + private readonly IJsonSerializer serializer; + private readonly string prefix; + private readonly ProjectionClient projectionClient; + + public GetEventStore(IEventStoreConnection connection, IJsonSerializer serializer, string prefix, string projectionHost) + { + Guard.NotNull(connection); + Guard.NotNull(serializer); + + this.connection = connection; + this.serializer = serializer; + + this.prefix = prefix.Trim(' ', '-').WithFallback("squidex"); + + projectionClient = new ProjectionClient(connection, prefix, projectionHost); + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + try + { + await connection.ConnectAsync(); + } + catch (Exception ex) + { + throw new ConfigurationException("Cannot connect to event store.", ex); + } + + await projectionClient.ConnectAsync(); + } + + public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string? streamFilter = null, string? position = null) + { + Guard.NotNull(streamFilter); + + return new GetEventStoreSubscription(connection, subscriber, serializer, projectionClient, position, prefix, streamFilter); + } + + public Task CreateIndexAsync(string property) + { + Guard.NotNullOrEmpty(property); + + return projectionClient.CreateProjectionAsync(property, string.Empty); + } + + public async Task QueryAsync(Func callback, string property, object value, string? position = null, CancellationToken ct = default) + { + Guard.NotNull(callback); + Guard.NotNullOrEmpty(property); + Guard.NotNull(value); + + using (Profiler.TraceMethod()) + { + var streamName = await projectionClient.CreateProjectionAsync(property, value); + + var sliceStart = projectionClient.ParsePosition(position); + + await QueryAsync(callback, streamName, sliceStart, ct); + } + } + + public async Task QueryAsync(Func callback, string? streamFilter = null, string? position = null, CancellationToken ct = default) + { + Guard.NotNull(callback); + + using (Profiler.TraceMethod()) + { + var streamName = await projectionClient.CreateProjectionAsync(streamFilter); + + var sliceStart = projectionClient.ParsePosition(position); + + await QueryAsync(callback, streamName, sliceStart, ct); + } + } + + private async Task QueryAsync(Func callback, string streamName, long sliceStart, CancellationToken ct = default) + { + StreamEventsSlice currentSlice; + do + { + currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, true); + + if (currentSlice.Status == SliceReadStatus.Success) + { + sliceStart = currentSlice.NextEventNumber; + + foreach (var resolved in currentSlice.Events) + { + var storedEvent = Formatter.Read(resolved, prefix, serializer); + + await callback(storedEvent); + } + } + } + while (!currentSlice.IsEndOfStream && !ct.IsCancellationRequested); + } + + public async Task> QueryAsync(string streamName, long streamPosition = 0) + { + Guard.NotNullOrEmpty(streamName); + + using (Profiler.TraceMethod()) + { + var result = new List(); + + var sliceStart = streamPosition >= 0 ? streamPosition : StreamPosition.Start; + + StreamEventsSlice currentSlice; + do + { + currentSlice = await connection.ReadStreamEventsForwardAsync(GetStreamName(streamName), sliceStart, ReadPageSize, true); + + if (currentSlice.Status == SliceReadStatus.Success) + { + sliceStart = currentSlice.NextEventNumber; + + foreach (var resolved in currentSlice.Events) + { + var storedEvent = Formatter.Read(resolved, prefix, serializer); + + result.Add(storedEvent); + } + } + } + while (!currentSlice.IsEndOfStream); + + return result; + } + } + + public Task DeleteStreamAsync(string streamName) + { + Guard.NotNullOrEmpty(streamName); + + return connection.DeleteStreamAsync(GetStreamName(streamName), ExpectedVersion.Any); + } + + public Task AppendAsync(Guid commitId, string streamName, ICollection events) + { + return AppendEventsInternalAsync(streamName, EtagVersion.Any, events); + } + + public Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + Guard.GreaterEquals(expectedVersion, -1); + + return AppendEventsInternalAsync(streamName, expectedVersion, events); + } + + private async Task AppendEventsInternalAsync(string streamName, long expectedVersion, ICollection events) + { + Guard.NotNullOrEmpty(streamName); + Guard.NotNull(events); + + using (Profiler.TraceMethod(nameof(AppendAsync))) + { + if (events.Count == 0) + { + return; + } + + try + { + var eventsToSave = events.Select(x => Formatter.Write(x, serializer)).ToList(); + + if (eventsToSave.Count < WritePageSize) + { + await connection.AppendToStreamAsync(GetStreamName(streamName), expectedVersion, eventsToSave); + } + else + { + using (var transaction = await connection.StartTransactionAsync(GetStreamName(streamName), expectedVersion)) + { + for (var p = 0; p < eventsToSave.Count; p += WritePageSize) + { + await transaction.WriteAsync(eventsToSave.Skip(p).Take(WritePageSize)); + } + + await transaction.CommitAsync(); + } + } + } + catch (WrongExpectedVersionException ex) + { + throw new WrongEventVersionException(ParseVersion(ex.Message), expectedVersion); + } + } + } + + private static int ParseVersion(string message) + { + return int.Parse(message.Substring(message.LastIndexOf(':') + 1)); + } + + private string GetStreamName(string streamName) + { + return $"{prefix}-{streamName}"; + } + } +} diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs new file mode 100644 index 000000000..3fa20e8ae --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs @@ -0,0 +1,81 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using EventStore.ClientAPI; +using EventStore.ClientAPI.Exceptions; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.EventSourcing +{ + internal sealed class GetEventStoreSubscription : IEventSubscription + { + private readonly IEventStoreConnection connection; + private readonly IEventSubscriber subscriber; + private readonly IJsonSerializer serializer; + private readonly string? prefix; + private readonly EventStoreCatchUpSubscription subscription; + private readonly long? position; + + public GetEventStoreSubscription( + IEventStoreConnection connection, + IEventSubscriber subscriber, + IJsonSerializer serializer, + ProjectionClient projectionClient, + string? position, + string? prefix, + string? streamFilter) + { + this.connection = connection; + + this.position = projectionClient.ParsePositionOrNull(position); + this.prefix = prefix; + + var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result; + + this.serializer = serializer; + this.subscriber = subscriber; + + subscription = SubscribeToStream(streamName); + } + + public Task StopAsync() + { + subscription.Stop(); + + return TaskHelper.Done; + } + + public void WakeUp() + { + } + + private EventStoreCatchUpSubscription SubscribeToStream(string streamName) + { + var settings = CatchUpSubscriptionSettings.Default; + + return connection.SubscribeToStreamFrom(streamName, position, settings, + (s, e) => + { + var storedEvent = Formatter.Read(e, prefix, serializer); + + subscriber.OnEventAsync(this, storedEvent).Wait(); + }, null, + (s, reason, ex) => + { + if (reason != SubscriptionDropReason.ConnectionClosed && + reason != SubscriptionDropReason.UserInitiated) + { + ex ??= new ConnectionClosedException($"Subscription closed with reason {reason}."); + + subscriber.OnErrorAsync(this, ex); + } + }); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs new file mode 100644 index 000000000..3136d7f0d --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using EventStore.ClientAPI; +using EventStore.ClientAPI.Exceptions; +using EventStore.ClientAPI.Projections; + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class ProjectionClient + { + private readonly ConcurrentDictionary projections = new ConcurrentDictionary(); + private readonly IEventStoreConnection connection; + private readonly string prefix; + private readonly string projectionHost; + private ProjectionsManager projectionsManager; + + public ProjectionClient(IEventStoreConnection connection, string prefix, string projectionHost) + { + this.connection = connection; + + this.prefix = prefix; + this.projectionHost = projectionHost; + } + + private string CreateFilterProjectionName(string filter) + { + return $"by-{prefix.Slugify()}-{filter.Slugify()}"; + } + + private string CreatePropertyProjectionName(string property) + { + return $"by-{prefix.Slugify()}-{property.Slugify()}-property"; + } + + public async Task CreateProjectionAsync(string property, object value) + { + var name = CreatePropertyProjectionName(property); + + var query = + $@"fromAll() + .when({{ + $any: function (s, e) {{ + if (e.streamId.indexOf('{prefix}') === 0 && e.metadata.{property}) {{ + linkTo('{name}-' + e.metadata.{property}, e); + }} + }} + }});"; + + await CreateProjectionAsync(name, query); + + return $"{name}-{value}"; + } + + public async Task CreateProjectionAsync(string? streamFilter = null) + { + streamFilter ??= ".*"; + + var name = CreateFilterProjectionName(streamFilter); + + var query = + $@"fromAll() + .when({{ + $any: function (s, e) {{ + if (e.streamId.indexOf('{prefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({prefix.Length + 1}))) {{ + linkTo('{name}', e); + }} + }} + }});"; + + await CreateProjectionAsync(name, query); + + return name; + } + + private async Task CreateProjectionAsync(string name, string query) + { + if (projections.TryAdd(name, true)) + { + try + { + var credentials = connection.Settings.DefaultUserCredentials; + + await projectionsManager.CreateContinuousAsync(name, query, credentials); + } + catch (Exception ex) + { + if (!ex.Is()) + { + throw; + } + } + } + } + + public async Task ConnectAsync() + { + var addressParts = projectionHost.Split(':'); + + if (addressParts.Length < 2 || !int.TryParse(addressParts[1], out var port)) + { + port = 2113; + } + + var endpoints = await Dns.GetHostAddressesAsync(addressParts[0]); + var endpoint = new IPEndPoint(endpoints.First(x => x.AddressFamily == AddressFamily.InterNetwork), port); + + projectionsManager = + new ProjectionsManager( + connection.Settings.Log, endpoint, + connection.Settings.OperationTimeout); + try + { + await projectionsManager.ListAllAsync(connection.Settings.DefaultUserCredentials); + } + catch (Exception ex) + { + throw new ConfigurationException($"Cannot connect to event store projections: {projectionHost}.", ex); + } + } + + public long? ParsePositionOrNull(string? position) + { + return long.TryParse(position, out var parsedPosition) ? (long?)parsedPosition : null; + } + + public long ParsePosition(string? position) + { + return long.TryParse(position, out var parsedPosition) ? parsedPosition + 1 : StreamPosition.Start; + } + } +} diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj b/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj new file mode 100644 index 000000000..1b7f280b3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj @@ -0,0 +1,26 @@ + + + netcoreapp3.0 + Squidex.Infrastructure + 8.0 + enable + + + full + True + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs b/backend/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs new file mode 100644 index 000000000..947c7c222 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs @@ -0,0 +1,112 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Google; +using Google.Cloud.Storage.V1; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class GoogleCloudAssetStore : IAssetStore, IInitializable + { + private static readonly UploadObjectOptions IfNotExists = new UploadObjectOptions { IfGenerationMatch = 0 }; + private static readonly CopyObjectOptions IfNotExistsCopy = new CopyObjectOptions { IfGenerationMatch = 0 }; + private readonly string bucketName; + private StorageClient storageClient; + + public GoogleCloudAssetStore(string bucketName) + { + Guard.NotNullOrEmpty(bucketName); + + this.bucketName = bucketName; + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + try + { + storageClient = StorageClient.Create(); + + await storageClient.GetBucketAsync(bucketName, cancellationToken: ct); + } + catch (Exception ex) + { + throw new ConfigurationException($"Cannot connect to google cloud bucket '${bucketName}'.", ex); + } + } + + public string? GeneratePublicUrl(string fileName) + { + return null; + } + + public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(sourceFileName); + Guard.NotNullOrEmpty(targetFileName); + + try + { + await storageClient.CopyObjectAsync(bucketName, sourceFileName, bucketName, targetFileName, IfNotExistsCopy, ct); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) + { + throw new AssetNotFoundException(sourceFileName, ex); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.PreconditionFailed) + { + throw new AssetAlreadyExistsException(targetFileName); + } + } + + public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + + try + { + await storageClient.DownloadObjectAsync(bucketName, fileName, stream, cancellationToken: ct); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) + { + throw new AssetNotFoundException(fileName, ex); + } + } + + public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + + try + { + await storageClient.UploadObjectAsync(bucketName, fileName, "application/octet-stream", stream, overwrite ? null : IfNotExists, ct); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.PreconditionFailed) + { + throw new AssetAlreadyExistsException(fileName); + } + } + + public async Task DeleteAsync(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + try + { + await storageClient.DeleteObjectAsync(bucketName, fileName); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) + { + return; + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj b/backend/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj new file mode 100644 index 000000000..fc3ff0be8 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj @@ -0,0 +1,27 @@ + + + netcoreapp3.0 + Squidex.Infrastructure + 8.0 + enable + + + full + True + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs new file mode 100644 index 000000000..6c585da51 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs @@ -0,0 +1,131 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.GridFS; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class MongoGridFsAssetStore : IAssetStore, IInitializable + { + private const int BufferSize = 81920; + private readonly IGridFSBucket bucket; + + public MongoGridFsAssetStore(IGridFSBucket bucket) + { + Guard.NotNull(bucket); + + this.bucket = bucket; + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + try + { + await bucket.Database.ListCollectionsAsync(cancellationToken: ct); + } + catch (MongoException ex) + { + throw new ConfigurationException($"Cannot connect to Mongo GridFS bucket '${bucket.Options.BucketName}'.", ex); + } + } + + public string? GeneratePublicUrl(string fileName) + { + return null; + } + + public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(targetFileName); + + try + { + var sourceName = GetFileName(sourceFileName, nameof(sourceFileName)); + + using (var readStream = await bucket.OpenDownloadStreamAsync(sourceFileName, cancellationToken: ct)) + { + await UploadAsync(targetFileName, readStream, false, ct); + } + } + catch (GridFSFileNotFoundException ex) + { + throw new AssetNotFoundException(sourceFileName, ex); + } + } + + public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + Guard.NotNull(stream); + + try + { + var name = GetFileName(fileName, nameof(fileName)); + + using (var readStream = await bucket.OpenDownloadStreamAsync(name, cancellationToken: ct)) + { + await readStream.CopyToAsync(stream, BufferSize, ct); + } + } + catch (GridFSFileNotFoundException ex) + { + throw new AssetNotFoundException(fileName, ex); + } + } + + public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + Guard.NotNull(stream); + + try + { + var name = GetFileName(fileName, nameof(fileName)); + + if (overwrite) + { + await DeleteAsync(fileName); + } + + await bucket.UploadFromStreamAsync(fileName, fileName, stream, cancellationToken: ct); + } + catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + throw new AssetAlreadyExistsException(fileName); + } + catch (MongoBulkWriteException ex) when (ex.WriteErrors.Any(x => x.Category == ServerErrorCategory.DuplicateKey)) + { + throw new AssetAlreadyExistsException(fileName); + } + } + + public async Task DeleteAsync(string fileName) + { + try + { + var name = GetFileName(fileName, nameof(fileName)); + + await bucket.DeleteAsync(name); + } + catch (GridFSFileNotFoundException) + { + return; + } + } + + private static string GetFileName(string fileName, string parameterName) + { + Guard.NotNullOrEmpty(fileName); + + return fileName; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs b/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs new file mode 100644 index 000000000..1f200b961 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using MongoDB.Driver; + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class MongoDBHealthCheck : IHealthCheck + { + private readonly IMongoDatabase mongoDatabase; + + public MongoDBHealthCheck(IMongoDatabase mongoDatabase) + { + Guard.NotNull(mongoDatabase); + + this.mongoDatabase = mongoDatabase; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var collectionNames = await mongoDatabase.ListCollectionNamesAsync(cancellationToken: cancellationToken); + + var result = await collectionNames.AnyAsync(cancellationToken); + + var status = result ? + HealthStatus.Healthy : + HealthStatus.Unhealthy; + + return new HealthCheckResult(status, "Application must query data from MongoDB"); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs rename to backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventCommit.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventCommit.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventCommit.cs rename to backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventCommit.cs diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs new file mode 100644 index 000000000..b8ed11d0c --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Infrastructure.EventSourcing +{ + public partial class MongoEventStore : MongoRepositoryBase, IEventStore + { + private static readonly FieldDefinition TimestampField = Fields.Build(x => x.Timestamp); + private static readonly FieldDefinition EventsCountField = Fields.Build(x => x.EventsCount); + private static readonly FieldDefinition EventStreamOffsetField = Fields.Build(x => x.EventStreamOffset); + private static readonly FieldDefinition EventStreamField = Fields.Build(x => x.EventStream); + private readonly IEventNotifier notifier; + + public IMongoCollection RawCollection + { + get { return Database.GetCollection(CollectionName()); } + } + + public MongoEventStore(IMongoDatabase database, IEventNotifier notifier) + : base(database) + { + Guard.NotNull(notifier); + + this.notifier = notifier; + } + + protected override string CollectionName() + { + return "Events"; + } + + protected override MongoCollectionSettings CollectionSettings() + { + return new MongoCollectionSettings { ReadPreference = ReadPreference.Primary, WriteConcern = WriteConcern.WMajority }; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel( + Index + .Ascending(x => x.Timestamp) + .Ascending(x => x.EventStream)), + new CreateIndexModel( + Index + .Ascending(x => x.EventStream) + .Descending(x => x.EventStreamOffset), + new CreateIndexOptions + { + Unique = true + }) + }, ct); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs new file mode 100644 index 000000000..757c19348 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs @@ -0,0 +1,210 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.MongoDb; +using EventFilter = MongoDB.Driver.FilterDefinition; + +namespace Squidex.Infrastructure.EventSourcing +{ + public delegate bool EventPredicate(EventData data); + + public partial class MongoEventStore : MongoRepositoryBase, IEventStore + { + public Task CreateIndexAsync(string property) + { + Guard.NotNullOrEmpty(property); + + return Collection.Indexes.CreateOneAsync( + new CreateIndexModel( + Index.Ascending(CreateIndexPath(property)))); + } + + public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string? streamFilter = null, string? position = null) + { + Guard.NotNull(subscriber); + + return new PollingSubscription(this, subscriber, streamFilter, position); + } + + public async Task> QueryAsync(string streamName, long streamPosition = 0) + { + Guard.NotNullOrEmpty(streamName); + + using (Profiler.TraceMethod()) + { + var commits = + await Collection.Find( + Filter.And( + Filter.Eq(EventStreamField, streamName), + Filter.Gte(EventStreamOffsetField, streamPosition - MaxCommitSize))) + .Sort(Sort.Ascending(TimestampField)).ToListAsync(); + + var result = new List(); + + foreach (var commit in commits) + { + var eventStreamOffset = (int)commit.EventStreamOffset; + + var commitTimestamp = commit.Timestamp; + var commitOffset = 0; + + foreach (var @event in commit.Events) + { + eventStreamOffset++; + + if (eventStreamOffset >= streamPosition) + { + var eventData = @event.ToEventData(); + var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + + result.Add(new StoredEvent(streamName, eventToken, eventStreamOffset, eventData)); + } + } + } + + return result; + } + } + + public Task QueryAsync(Func callback, string property, object value, string? position = null, CancellationToken ct = default) + { + Guard.NotNull(callback); + Guard.NotNullOrEmpty(property); + Guard.NotNull(value); + + StreamPosition lastPosition = position; + + var filterDefinition = CreateFilter(property, value, lastPosition); + var filterExpression = CreateFilterExpression(property, value); + + return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); + } + + public Task QueryAsync(Func callback, string? streamFilter = null, string? position = null, CancellationToken ct = default) + { + Guard.NotNull(callback); + + StreamPosition lastPosition = position; + + var filterDefinition = CreateFilter(streamFilter, lastPosition); + var filterExpression = CreateFilterExpression(null, null); + + return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); + } + + private async Task QueryAsync(Func callback, StreamPosition lastPosition, EventFilter filterDefinition, EventPredicate filterExpression, CancellationToken ct = default) + { + using (Profiler.TraceMethod()) + { + await Collection.Find(filterDefinition, options: Batching.Options).Sort(Sort.Ascending(TimestampField)).ForEachPipelineAsync(async commit => + { + var eventStreamOffset = (int)commit.EventStreamOffset; + + var commitTimestamp = commit.Timestamp; + var commitOffset = 0; + + foreach (var @event in commit.Events) + { + eventStreamOffset++; + + if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) + { + var eventData = @event.ToEventData(); + + if (filterExpression(eventData)) + { + var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + + await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData)); + } + } + + commitOffset++; + } + }, ct); + } + } + + private static EventFilter CreateFilter(string property, object value, StreamPosition streamPosition) + { + var filters = new List(); + + AppendByPosition(streamPosition, filters); + AppendByProperty(property, value, filters); + + return Filter.And(filters); + } + + private static EventFilter CreateFilter(string? streamFilter, StreamPosition streamPosition) + { + var filters = new List(); + + AppendByPosition(streamPosition, filters); + AppendByStream(streamFilter, filters); + + return Filter.And(filters); + } + + private static void AppendByProperty(string property, object value, List filters) + { + filters.Add(Filter.Eq(CreateIndexPath(property), value)); + } + + private static void AppendByStream(string? streamFilter, List filters) + { + if (!StreamFilter.IsAll(streamFilter)) + { + if (streamFilter.Contains("^")) + { + filters.Add(Filter.Regex(EventStreamField, streamFilter)); + } + else + { + filters.Add(Filter.Eq(EventStreamField, streamFilter)); + } + } + } + + private static void AppendByPosition(StreamPosition streamPosition, List filters) + { + if (streamPosition.IsEndOfCommit) + { + filters.Add(Filter.Gt(TimestampField, streamPosition.Timestamp)); + } + else + { + filters.Add(Filter.Gte(TimestampField, streamPosition.Timestamp)); + } + } + + private static EventPredicate CreateFilterExpression(string? property, object? value) + { + if (!string.IsNullOrWhiteSpace(property)) + { + var jsonValue = JsonValue.Create(value); + + return x => x.Headers.TryGetValue(property, out var p) && p.Equals(jsonValue); + } + else + { + return x => true; + } + } + + private static string CreateIndexPath(string property) + { + return $"Events.Metadata.{property}"; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs new file mode 100644 index 000000000..5b50bbe90 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs @@ -0,0 +1,144 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.EventSourcing +{ + public partial class MongoEventStore + { + private const int MaxCommitSize = 10; + private const int MaxWriteAttempts = 20; + private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0); + + public Task DeleteStreamAsync(string streamName) + { + Guard.NotNullOrEmpty(streamName); + + return Collection.DeleteManyAsync(x => x.EventStream == streamName); + } + + public Task AppendAsync(Guid commitId, string streamName, ICollection events) + { + return AppendAsync(commitId, streamName, EtagVersion.Any, events); + } + + public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + Guard.NotEmpty(commitId); + Guard.NotNullOrEmpty(streamName); + Guard.NotNull(events); + Guard.NotNullOrEmpty(streamName); + Guard.NotNull(events); + Guard.LessThan(events.Count, MaxCommitSize, "events.Count"); + Guard.GreaterEquals(expectedVersion, EtagVersion.Any); + + using (Profiler.TraceMethod()) + { + if (events.Count == 0) + { + return; + } + + var currentVersion = await GetEventStreamOffsetAsync(streamName); + + if (expectedVersion > EtagVersion.Any && expectedVersion != currentVersion) + { + throw new WrongEventVersionException(currentVersion, expectedVersion); + } + + var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events); + + for (var attempt = 0; attempt < MaxWriteAttempts; attempt++) + { + try + { + await Collection.InsertOneAsync(commit); + + notifier.NotifyEventsStored(streamName); + + return; + } + catch (MongoWriteException ex) + { + if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) + { + currentVersion = await GetEventStreamOffsetAsync(streamName); + + if (expectedVersion > EtagVersion.Any) + { + throw new WrongEventVersionException(currentVersion, expectedVersion); + } + + if (attempt < MaxWriteAttempts) + { + expectedVersion = currentVersion; + } + else + { + throw new TimeoutException("Could not acquire a free slot for the commit within the provided time."); + } + } + else + { + throw; + } + } + } + } + } + + private async Task GetEventStreamOffsetAsync(string streamName) + { + var document = + await Collection.Find(Filter.Eq(EventStreamField, streamName)) + .Project(Projection + .Include(EventStreamOffsetField) + .Include(EventsCountField)) + .Sort(Sort.Descending(EventStreamOffsetField)).Limit(1) + .FirstOrDefaultAsync(); + + if (document != null) + { + return document[nameof(MongoEventCommit.EventStreamOffset)].ToInt64() + document[nameof(MongoEventCommit.EventsCount)].ToInt64(); + } + + return EtagVersion.Empty; + } + + private static MongoEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + var commitEvents = new MongoEvent[events.Count]; + + var i = 0; + + foreach (var e in events) + { + var mongoEvent = MongoEvent.FromEventData(e); + + commitEvents[i++] = mongoEvent; + } + + var mongoCommit = new MongoEventCommit + { + Id = commitId, + Events = commitEvents, + EventsCount = events.Count, + EventStream = streamName, + EventStreamOffset = expectedVersion, + Timestamp = EmptyTimestamp + }; + + return mongoCommit; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs new file mode 100644 index 000000000..dcfb32884 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs @@ -0,0 +1,60 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Bson; + +namespace Squidex.Infrastructure.EventSourcing +{ + internal sealed class StreamPosition + { + private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(946681200, 0); + + public BsonTimestamp Timestamp { get; } + + public long CommitOffset { get; } + + public long CommitSize { get; } + + public bool IsEndOfCommit + { + get { return CommitOffset == CommitSize - 1; } + } + + public StreamPosition(BsonTimestamp timestamp, long commitOffset, long commitSize) + { + Timestamp = timestamp; + + CommitOffset = commitOffset; + CommitSize = commitSize; + } + + public static implicit operator string(StreamPosition position) + { + var parts = new object[] + { + position.Timestamp.Timestamp, + position.Timestamp.Increment, + position.CommitOffset, + position.CommitSize + }; + + return string.Join("-", parts); + } + + public static implicit operator StreamPosition(string? position) + { + if (!string.IsNullOrWhiteSpace(position)) + { + var parts = position.Split('-'); + + return new StreamPosition(new BsonTimestamp(int.Parse(parts[0]), int.Parse(parts[1])), long.Parse(parts[2]), long.Parse(parts[3])); + } + + return new StreamPosition(EmptyTimestamp, -1, -1); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs b/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs rename to backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs diff --git a/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs b/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs rename to backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs new file mode 100644 index 000000000..671b2caba --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Reflection; +using System.Threading; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Conventions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Squidex.Infrastructure.MongoDb +{ + public static class BsonJsonConvention + { + private static volatile int isRegistered; + + public static void Register(JsonSerializer serializer) + { + if (Interlocked.Increment(ref isRegistered) == 1) + { + var pack = new ConventionPack(); + + pack.AddMemberMapConvention("JsonBson", memberMap => + { + var attributes = memberMap.MemberInfo.GetCustomAttributes(); + + if (attributes.OfType().Any()) + { + var bsonSerializerType = typeof(BsonJsonSerializer<>).MakeGenericType(memberMap.MemberType); + var bsonSerializer = Activator.CreateInstance(bsonSerializerType, serializer); + + memberMap.SetSerializer((IBsonSerializer)bsonSerializer!); + } + else if (memberMap.MemberType == typeof(JToken)) + { + memberMap.SetSerializer(JTokenSerializer.Instance); + } + else if (memberMap.MemberType == typeof(JObject)) + { + memberMap.SetSerializer(JTokenSerializer.Instance); + } + else if (memberMap.MemberType == typeof(JValue)) + { + memberMap.SetSerializer(JTokenSerializer.Instance); + } + }); + + ConventionRegistry.Register("json", pack, t => true); + } + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs new file mode 100644 index 000000000..87657127b --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs @@ -0,0 +1,107 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using MongoDB.Bson; +using MongoDB.Bson.IO; +using NewtonsoftJsonReader = Newtonsoft.Json.JsonReader; +using NewtonsoftJsonToken = Newtonsoft.Json.JsonToken; + +namespace Squidex.Infrastructure.MongoDb +{ + public sealed class BsonJsonReader : NewtonsoftJsonReader + { + private readonly IBsonReader bsonReader; + + public BsonJsonReader(IBsonReader bsonReader) + { + Guard.NotNull(bsonReader); + + this.bsonReader = bsonReader; + } + + public override bool Read() + { + if (bsonReader.State == BsonReaderState.Initial || + bsonReader.State == BsonReaderState.ScopeDocument || + bsonReader.State == BsonReaderState.Type) + { + bsonReader.ReadBsonType(); + } + + if (bsonReader.State == BsonReaderState.Name) + { + SetToken(NewtonsoftJsonToken.PropertyName, bsonReader.ReadName().UnescapeBson()); + } + else if (bsonReader.State == BsonReaderState.Value) + { + switch (bsonReader.CurrentBsonType) + { + case BsonType.Document: + SetToken(NewtonsoftJsonToken.StartObject); + bsonReader.ReadStartDocument(); + break; + case BsonType.Array: + SetToken(NewtonsoftJsonToken.StartArray); + bsonReader.ReadStartArray(); + break; + case BsonType.Undefined: + SetToken(NewtonsoftJsonToken.Undefined); + bsonReader.ReadUndefined(); + break; + case BsonType.Null: + SetToken(NewtonsoftJsonToken.Null); + bsonReader.ReadNull(); + break; + case BsonType.String: + SetToken(NewtonsoftJsonToken.String, bsonReader.ReadString()); + break; + case BsonType.Binary: + SetToken(NewtonsoftJsonToken.Bytes, bsonReader.ReadBinaryData().Bytes); + break; + case BsonType.Boolean: + SetToken(NewtonsoftJsonToken.Boolean, bsonReader.ReadBoolean()); + break; + case BsonType.DateTime: + SetToken(NewtonsoftJsonToken.Date, bsonReader.ReadDateTime()); + break; + case BsonType.Int32: + SetToken(NewtonsoftJsonToken.Integer, bsonReader.ReadInt32()); + break; + case BsonType.Int64: + SetToken(NewtonsoftJsonToken.Integer, bsonReader.ReadInt64()); + break; + case BsonType.Double: + SetToken(NewtonsoftJsonToken.Float, bsonReader.ReadDouble()); + break; + case BsonType.Decimal128: + SetToken(NewtonsoftJsonToken.Float, Decimal128.ToDouble(bsonReader.ReadDecimal128())); + break; + default: + throw new NotSupportedException(); + } + } + else if (bsonReader.State == BsonReaderState.EndOfDocument) + { + SetToken(NewtonsoftJsonToken.EndObject); + bsonReader.ReadEndDocument(); + } + else if (bsonReader.State == BsonReaderState.EndOfArray) + { + SetToken(NewtonsoftJsonToken.EndArray); + bsonReader.ReadEndArray(); + } + + if (bsonReader.State == BsonReaderState.Initial) + { + return true; + } + + return !bsonReader.IsAtEndOfFile(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs new file mode 100644 index 000000000..f5896212f --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs @@ -0,0 +1,60 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using Newtonsoft.Json; + +namespace Squidex.Infrastructure.MongoDb +{ + public sealed class BsonJsonSerializer : ClassSerializerBase where T : class + { + private readonly JsonSerializer serializer; + + public BsonJsonSerializer(JsonSerializer serializer) + { + Guard.NotNull(serializer); + + this.serializer = serializer; + } + + public override T? Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var bsonReader = context.Reader; + + if (bsonReader.GetCurrentBsonType() == BsonType.Null) + { + bsonReader.ReadNull(); + + return null; + } + else + { + var jsonReader = new BsonJsonReader(bsonReader); + + return serializer.Deserialize(jsonReader); + } + } + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T? value) + { + var bsonWriter = context.Writer; + + if (value == null) + { + bsonWriter.WriteNull(); + } + else + { + var jsonWriter = new BsonJsonWriter(bsonWriter); + + serializer.Serialize(jsonWriter, value); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs new file mode 100644 index 000000000..22f52e559 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs @@ -0,0 +1,178 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Globalization; +using MongoDB.Bson.IO; +using NewtonsoftJSonWriter = Newtonsoft.Json.JsonWriter; + +namespace Squidex.Infrastructure.MongoDb +{ + public sealed class BsonJsonWriter : NewtonsoftJSonWriter + { + private readonly IBsonWriter bsonWriter; + + public BsonJsonWriter(IBsonWriter bsonWriter) + { + Guard.NotNull(bsonWriter); + + this.bsonWriter = bsonWriter; + } + + public override void WritePropertyName(string name) + { + bsonWriter.WriteName(name.EscapeJson()); + } + + public override void WritePropertyName(string name, bool escape) + { + bsonWriter.WriteName(name.EscapeJson()); + } + + public override void WriteStartArray() + { + bsonWriter.WriteStartArray(); + } + + public override void WriteEndArray() + { + bsonWriter.WriteEndArray(); + } + + public override void WriteStartObject() + { + bsonWriter.WriteStartDocument(); + } + + public override void WriteEndObject() + { + bsonWriter.WriteEndDocument(); + } + + public override void WriteNull() + { + bsonWriter.WriteNull(); + } + + public override void WriteUndefined() + { + bsonWriter.WriteUndefined(); + } + + public override void WriteValue(string value) + { + bsonWriter.WriteString(value); + } + + public override void WriteValue(int value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(uint value) + { + bsonWriter.WriteInt32((int)value); + } + + public override void WriteValue(long value) + { + bsonWriter.WriteInt64(value); + } + + public override void WriteValue(ulong value) + { + bsonWriter.WriteInt64((long)value); + } + + public override void WriteValue(float value) + { + bsonWriter.WriteDouble(value); + } + + public override void WriteValue(double value) + { + bsonWriter.WriteDouble(value); + } + + public override void WriteValue(bool value) + { + bsonWriter.WriteBoolean(value); + } + + public override void WriteValue(short value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(ushort value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(char value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(byte value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(sbyte value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(decimal value) + { + bsonWriter.WriteDecimal128(value); + } + + public override void WriteValue(DateTime value) + { + bsonWriter.WriteString(value.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); + } + + public override void WriteValue(DateTimeOffset value) + { + if (value.Offset == TimeSpan.Zero) + { + bsonWriter.WriteString(value.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); + } + else + { + bsonWriter.WriteString(value.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); + } + } + + public override void WriteValue(byte[] value) + { + bsonWriter.WriteBytes(value); + } + + public override void WriteValue(TimeSpan value) + { + bsonWriter.WriteString(value.ToString()); + } + + public override void WriteValue(Guid value) + { + bsonWriter.WriteString(value.ToString()); + } + + public override void WriteValue(Uri value) + { + bsonWriter.WriteString(value.ToString()); + } + + public override void Flush() + { + bsonWriter.Flush(); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/IVersionedEntity.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/IVersionedEntity.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/IVersionedEntity.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/IVersionedEntity.cs diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs new file mode 100644 index 000000000..ccdc54493 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using Newtonsoft.Json.Linq; + +namespace Squidex.Infrastructure.MongoDb +{ + public sealed class JTokenSerializer : ClassSerializerBase where T : JToken + { + public static readonly JTokenSerializer Instance = new JTokenSerializer(); + + public override T? Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var bsonReader = context.Reader; + + if (bsonReader.GetCurrentBsonType() == BsonType.Null) + { + bsonReader.ReadNull(); + + return null; + } + else + { + var jsonReader = new BsonJsonReader(bsonReader); + + return (T)JToken.ReadFrom(jsonReader); + } + } + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T? value) + { + var bsonWriter = context.Writer; + + if (value == null) + { + bsonWriter.WriteNull(); + } + else + { + var jsonWriter = new BsonJsonWriter(bsonWriter); + + value.WriteTo(jsonWriter); + } + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoEntity.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoEntity.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/MongoEntity.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoEntity.cs diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs new file mode 100644 index 000000000..dae2fc7af --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs @@ -0,0 +1,216 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Infrastructure.States; + +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body + +namespace Squidex.Infrastructure.MongoDb +{ + public static class MongoExtensions + { + private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; + + public static async Task CollectionExistsAsync(this IMongoDatabase database, string collectionName) + { + var options = new ListCollectionNamesOptions + { + Filter = new BsonDocument("name", collectionName) + }; + + return (await database.ListCollectionNamesAsync(options)).Any(); + } + + public static async Task InsertOneIfNotExistsAsync(this IMongoCollection collection, T document) + { + try + { + await collection.InsertOneAsync(document); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + return false; + } + + throw; + } + + return true; + } + + public static async Task TryDropOneAsync(this IMongoIndexManager indexes, string name) + { + try + { + await indexes.DropOneAsync(name); + } + catch + { + /* NOOP */ + } + } + + public static IFindFluent Only(this IFindFluent find, + Expression> include) + { + return find.Project(Builders.Projection.Include(include)); + } + + public static IFindFluent Only(this IFindFluent find, + Expression> include1, + Expression> include2) + { + return find.Project(Builders.Projection.Include(include1).Include(include2)); + } + + public static IFindFluent Only(this IFindFluent find, + Expression> include1, + Expression> include2, + Expression> include3) + { + return find.Project(Builders.Projection.Include(include1).Include(include2).Include(include3)); + } + + public static IFindFluent Not(this IFindFluent find, + Expression> exclude) + { + return find.Project(Builders.Projection.Exclude(exclude)); + } + + public static IFindFluent Not(this IFindFluent find, + Expression> exclude1, + Expression> exclude2) + { + return find.Project(Builders.Projection.Exclude(exclude1).Exclude(exclude2)); + } + + public static IFindFluent Not(this IFindFluent find, + Expression> exclude1, + Expression> exclude2, + Expression> exclude3) + { + return find.Project(Builders.Projection.Exclude(exclude1).Exclude(exclude2).Exclude(exclude3)); + } + + public static async Task UpsertVersionedAsync(this IMongoCollection collection, TKey key, long oldVersion, long newVersion, Func, UpdateDefinition> updater) where T : IVersionedEntity where TKey : notnull + { + try + { + var update = updater(Builders.Update.Set(x => x.Version, newVersion)); + + await collection.UpdateOneAsync(x => x.Id.Equals(key) && x.Version == oldVersion, update, Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await collection.Find(x => x.Id.Equals(key)).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion[nameof(IVersionedEntity.Version)].AsInt64, oldVersion, ex); + } + } + else + { + throw; + } + } + } + + public static async Task UpsertVersionedAsync(this IMongoCollection collection, TKey key, long oldVersion, T doc) where T : IVersionedEntity where TKey : notnull + { + try + { + await collection.ReplaceOneAsync(x => x.Id.Equals(key) && x.Version == oldVersion, doc, Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await collection.Find(x => x.Id.Equals(key)).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion[nameof(IVersionedEntity.Version)].AsInt64, oldVersion, ex); + } + } + else + { + throw; + } + } + } + + public static async Task ForEachPipelineAsync(this IAsyncCursorSource source, Func processor, CancellationToken cancellationToken = default) + { + using (var cursor = await source.ToCursorAsync(cancellationToken)) + { + await cursor.ForEachPipelineAsync(processor, cancellationToken); + } + } + + public static async Task ForEachPipelineAsync(this IAsyncCursor source, Func processor, CancellationToken cancellationToken = default) + { + using (var selfToken = new CancellationTokenSource()) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(selfToken.Token, cancellationToken)) + { + var actionBlock = + new ActionBlock(async x => + { + if (!combined.IsCancellationRequested) + { + await processor(x); + } + }, + new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = 1, + MaxMessagesPerTask = 1, + BoundedCapacity = Batching.BufferSize + }); + try + { + await source.ForEachAsync(async i => + { + var t = source; + + if (!await actionBlock.SendAsync(i, combined.Token)) + { + selfToken.Cancel(); + } + }, combined.Token); + + actionBlock.Complete(); + } + catch (Exception ex) + { + ((IDataflowBlock)actionBlock).Fault(ex); + } + finally + { + await actionBlock.Completion; + } + } + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs new file mode 100644 index 000000000..d452dab6d --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -0,0 +1,101 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Infrastructure.Tasks; + +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Infrastructure.MongoDb +{ + public abstract class MongoRepositoryBase : IInitializable + { + private const string CollectionFormat = "{0}Set"; + + protected static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; + protected static readonly SortDefinitionBuilder Sort = Builders.Sort; + protected static readonly UpdateDefinitionBuilder Update = Builders.Update; + protected static readonly FieldDefinitionBuilder Fields = FieldDefinitionBuilder.Instance; + protected static readonly FilterDefinitionBuilder Filter = Builders.Filter; + protected static readonly IndexKeysDefinitionBuilder Index = Builders.IndexKeys; + protected static readonly ProjectionDefinitionBuilder Projection = Builders.Projection; + + private readonly IMongoDatabase mongoDatabase; + private Lazy> mongoCollection; + + protected IMongoCollection Collection + { + get { return mongoCollection.Value; } + } + + protected IMongoDatabase Database + { + get { return mongoDatabase; } + } + + static MongoRepositoryBase() + { + RefTokenSerializer.Register(); + + InstantSerializer.Register(); + } + + protected MongoRepositoryBase(IMongoDatabase database) + { + Guard.NotNull(database); + + mongoDatabase = database; + mongoCollection = CreateCollection(); + } + + protected virtual MongoCollectionSettings CollectionSettings() + { + return new MongoCollectionSettings(); + } + + protected virtual string CollectionName() + { + return string.Format(CultureInfo.InvariantCulture, CollectionFormat, typeof(TEntity).Name); + } + + private Lazy> CreateCollection() + { + return new Lazy>(() => + mongoDatabase.GetCollection( + CollectionName(), + CollectionSettings() ?? new MongoCollectionSettings())); + } + + protected virtual Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + return TaskHelper.Done; + } + + public virtual async Task ClearAsync() + { + await Database.DropCollectionAsync(CollectionName()); + + await SetupCollectionAsync(Collection); + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + try + { + await SetupCollectionAsync(Collection, ct); + } + catch (Exception ex) + { + throw new ConfigurationException($"MongoDb connection failed to connect to database {Database.DatabaseNamespace.DatabaseName}", ex); + } + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs new file mode 100644 index 000000000..995af9d52 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Driver; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Infrastructure.MongoDb.Queries +{ + public static class FilterBuilder + { + public static (FilterDefinition? Filter, bool Last) BuildFilter(this ClrQuery query, bool supportsSearch = true) + { + if (query.FullText != null) + { + if (!supportsSearch) + { + throw new ValidationException("Query $search clause not supported."); + } + + return (Builders.Filter.Text(query.FullText), false); + } + + if (query.Filter != null) + { + return (query.Filter.BuildFilter(), true); + } + + return (null, false); + } + + public static FilterDefinition BuildFilter(this FilterNode filterNode) + { + return FilterVisitor.Visit(filterNode); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs new file mode 100644 index 000000000..c3dc47147 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs @@ -0,0 +1,92 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections; +using System.Linq; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Infrastructure.MongoDb.Queries +{ + public sealed class FilterVisitor : FilterNodeVisitor, ClrValue> + { + private static readonly FilterDefinitionBuilder Filter = Builders.Filter; + private static readonly FilterVisitor Instance = new FilterVisitor(); + + private FilterVisitor() + { + } + + public static FilterDefinition Visit(FilterNode node) + { + return node.Accept(Instance); + } + + public override FilterDefinition Visit(NegateFilter nodeIn) + { + return Filter.Not(nodeIn.Filter.Accept(this)); + } + + public override FilterDefinition Visit(LogicalFilter nodeIn) + { + if (nodeIn.Type == LogicalFilterType.And) + { + return Filter.And(nodeIn.Filters.Select(x => x.Accept(this))); + } + else + { + return Filter.Or(nodeIn.Filters.Select(x => x.Accept(this))); + } + } + + public override FilterDefinition Visit(CompareFilter nodeIn) + { + var propertyName = nodeIn.Path.ToString(); + + var value = nodeIn.Value.Value; + + switch (nodeIn.Operator) + { + case CompareOperator.Empty: + return Filter.Or( + Filter.Exists(propertyName, false), + Filter.Eq(propertyName, default(T)!), + Filter.Eq(propertyName, string.Empty), + Filter.Eq(propertyName, new T[0])); + case CompareOperator.StartsWith: + return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "^" + s)); + case CompareOperator.Contains: + return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s)); + case CompareOperator.EndsWith: + return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s + "$")); + case CompareOperator.Equals: + return Filter.Eq(propertyName, value); + case CompareOperator.GreaterThan: + return Filter.Gt(propertyName, value); + case CompareOperator.GreaterThanOrEqual: + return Filter.Gte(propertyName, value); + case CompareOperator.LessThan: + return Filter.Lt(propertyName, value); + case CompareOperator.LessThanOrEqual: + return Filter.Lte(propertyName, value); + case CompareOperator.NotEquals: + return Filter.Ne(propertyName, value); + case CompareOperator.In: + return Filter.In(propertyName, ((IList)value!).OfType()); + } + + throw new NotSupportedException(); + } + + private static BsonRegularExpression BuildRegex(CompareFilter node, Func formatter) + { + return new BsonRegularExpression(formatter(node.Value.Value!.ToString()!), "i"); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs new file mode 100644 index 000000000..0ec5b5db4 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using MongoDB.Driver; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Infrastructure.MongoDb.Queries +{ + public static class SortBuilder + { + public static SortDefinition? BuildSort(this ClrQuery query) + { + if (query.Sort.Count > 0) + { + var sorts = new List>(); + + foreach (var sort in query.Sort) + { + sorts.Add(OrderBy(sort)); + } + + if (sorts.Count > 1) + { + return Builders.Sort.Combine(sorts); + } + else + { + return sorts[0]; + } + } + + return null; + } + + public static SortDefinition OrderBy(SortNode sort) + { + var propertyName = string.Join(".", sort.Path); + + if (sort.Order == SortOrder.Ascending) + { + return Builders.Sort.Ascending(propertyName); + } + else + { + return Builders.Sort.Descending(propertyName); + } + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj new file mode 100644 index 000000000..9a73cc684 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj @@ -0,0 +1,29 @@ + + + netcoreapp3.0 + Squidex.Infrastructure + 8.0 + enable + + + full + True + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs new file mode 100644 index 000000000..647324ea7 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Newtonsoft.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Infrastructure.States +{ + public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore where TKey : notnull + { + public MongoSnapshotStore(IMongoDatabase database, JsonSerializer jsonSerializer) + : base(database) + { + Guard.NotNull(jsonSerializer); + + BsonJsonConvention.Register(jsonSerializer); + } + + protected override string CollectionName() + { + var attribute = typeof(T).GetCustomAttributes(true).OfType().FirstOrDefault(); + + var name = attribute?.Name ?? typeof(T).Name; + + return $"States_{name}"; + } + + public async Task<(T Value, long Version)> ReadAsync(TKey key) + { + using (Profiler.TraceMethod>()) + { + var existing = + await Collection.Find(x => x.Id.Equals(key)) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (existing.Doc, existing.Version); + } + + return (default, EtagVersion.NotFound); + } + } + + public async Task WriteAsync(TKey key, T value, long oldVersion, long newVersion) + { + using (Profiler.TraceMethod>()) + { + await Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u.Set(x => x.Doc, value)); + } + } + + public async Task ReadAllAsync(Func callback, CancellationToken ct = default) + { + using (Profiler.TraceMethod>()) + { + await Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(x => callback(x.Doc, x.Version), ct); + } + } + + public async Task RemoveAsync(TKey key) + { + using (Profiler.TraceMethod>()) + { + await Collection.DeleteOneAsync(x => x.Id.Equals(key)); + } + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/States/MongoState.cs rename to backend/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs diff --git a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs b/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs rename to backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs diff --git a/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs b/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs new file mode 100644 index 000000000..e71a14257 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs @@ -0,0 +1,105 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Infrastructure.UsageTracking +{ + public sealed class MongoUsageRepository : MongoRepositoryBase, IUsageRepository + { + private static readonly BulkWriteOptions Unordered = new BulkWriteOptions { IsOrdered = false }; + + public MongoUsageRepository(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "UsagesV2"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + return collection.Indexes.CreateOneAsync( + new CreateIndexModel( + Index + .Ascending(x => x.Key) + .Ascending(x => x.Category) + .Ascending(x => x.Date)), + cancellationToken: ct); + } + + public async Task TrackUsagesAsync(UsageUpdate update) + { + Guard.NotNull(update); + + if (update.Counters.Count > 0) + { + var (filter, updateStatement) = CreateOperation(update); + + await Collection.UpdateOneAsync(filter, updateStatement, Upsert); + } + } + + public async Task TrackUsagesAsync(params UsageUpdate[] updates) + { + if (updates.Length == 1) + { + await TrackUsagesAsync(updates[0]); + } + else if (updates.Length > 0) + { + var writes = new List>(); + + foreach (var update in updates) + { + if (update.Counters.Count > 0) + { + var (filter, updateStatement) = CreateOperation(update); + + writes.Add(new UpdateOneModel(filter, updateStatement) { IsUpsert = true }); + } + } + + await Collection.BulkWriteAsync(writes, Unordered); + } + } + + private static (FilterDefinition, UpdateDefinition) CreateOperation(UsageUpdate usageUpdate) + { + var id = $"{usageUpdate.Key}_{usageUpdate.Date:yyyy-MM-dd}_{usageUpdate.Category}"; + + var update = Update + .SetOnInsert(x => x.Key, usageUpdate.Key) + .SetOnInsert(x => x.Date, usageUpdate.Date) + .SetOnInsert(x => x.Category, usageUpdate.Category); + + foreach (var counter in usageUpdate.Counters) + { + update = update.Inc($"Counters.{counter.Key}", counter.Value); + } + + var filter = Filter.Eq(x => x.Id, id); + + return (filter, update); + } + + public async Task> QueryAsync(string key, DateTime fromDate, DateTime toDate) + { + var entities = await Collection.Find(x => x.Key == key && x.Date >= fromDate && x.Date <= toDate).ToListAsync(); + + return entities.Select(x => new StoredUsage(x.Category, x.Date, x.Counters)).ToList(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs b/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs new file mode 100644 index 000000000..4bbaaf222 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.CQRS.Events +{ + public sealed class RabbitMqEventConsumer : DisposableObjectBase, IInitializable, IEventConsumer + { + private readonly IJsonSerializer jsonSerializer; + private readonly string eventPublisherName; + private readonly string exchange; + private readonly string eventsFilter; + private readonly ConnectionFactory connectionFactory; + private readonly Lazy connection; + private readonly Lazy channel; + + public string Name + { + get { return eventPublisherName; } + } + + public string EventsFilter + { + get { return eventsFilter; } + } + + public RabbitMqEventConsumer(IJsonSerializer jsonSerializer, string eventPublisherName, string uri, string exchange, string eventsFilter) + { + Guard.NotNullOrEmpty(uri); + Guard.NotNullOrEmpty(eventPublisherName); + Guard.NotNullOrEmpty(exchange); + Guard.NotNull(jsonSerializer); + + connectionFactory = new ConnectionFactory { Uri = new Uri(uri, UriKind.Absolute) }; + connection = new Lazy(connectionFactory.CreateConnection); + channel = new Lazy(connection.Value.CreateModel); + + this.exchange = exchange; + this.eventsFilter = eventsFilter; + this.eventPublisherName = eventPublisherName; + this.jsonSerializer = jsonSerializer; + } + + protected override void DisposeObject(bool disposing) + { + if (connection.IsValueCreated) + { + connection.Value.Close(); + connection.Value.Dispose(); + } + } + + public Task InitializeAsync(CancellationToken ct = default) + { + try + { + var currentConnection = connection.Value; + + if (!currentConnection.IsOpen) + { + throw new ConfigurationException($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}"); + } + + return TaskHelper.Done; + } + catch (Exception e) + { + throw new ConfigurationException($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}", e); + } + } + + public bool Handles(StoredEvent @event) + { + return true; + } + + public Task ClearAsync() + { + return TaskHelper.Done; + } + + public Task On(Envelope @event) + { + var jsonString = jsonSerializer.Serialize(@event); + var jsonBytes = Encoding.UTF8.GetBytes(jsonString); + + channel.Value.BasicPublish(exchange, string.Empty, null, jsonBytes); + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj b/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj new file mode 100644 index 000000000..37b1669f6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj @@ -0,0 +1,27 @@ + + + netcoreapp3.0 + Squidex.Infrastructure + 8.0 + enable + + + full + True + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/src/Squidex.Infrastructure.Redis/RedisPubSub.cs b/backend/src/Squidex.Infrastructure.Redis/RedisPubSub.cs similarity index 100% rename from src/Squidex.Infrastructure.Redis/RedisPubSub.cs rename to backend/src/Squidex.Infrastructure.Redis/RedisPubSub.cs diff --git a/src/Squidex.Infrastructure.Redis/RedisSubscription.cs b/backend/src/Squidex.Infrastructure.Redis/RedisSubscription.cs similarity index 100% rename from src/Squidex.Infrastructure.Redis/RedisSubscription.cs rename to backend/src/Squidex.Infrastructure.Redis/RedisSubscription.cs diff --git a/backend/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj b/backend/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj new file mode 100644 index 000000000..6edffe3ec --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj @@ -0,0 +1,26 @@ + + + netcoreapp3.0 + Squidex.Infrastructure + 7.3 + + + full + True + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/backend/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs b/backend/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs new file mode 100644 index 000000000..1ab0d2964 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.Serialization; + +namespace Squidex.Infrastructure.Assets +{ + [Serializable] + public class AssetAlreadyExistsException : Exception + { + public AssetAlreadyExistsException(string fileName) + : base(FormatMessage(fileName)) + { + } + + public AssetAlreadyExistsException(string fileName, Exception inner) + : base(FormatMessage(fileName), inner) + { + } + + protected AssetAlreadyExistsException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + private static string FormatMessage(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + return $"An asset with name '{fileName}' already exists."; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/AssetFile.cs b/backend/src/Squidex.Infrastructure/Assets/AssetFile.cs new file mode 100644 index 000000000..9f5a9aff3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/AssetFile.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class AssetFile + { + private readonly Func openAction; + + public string FileName { get; } + + public string MimeType { get; } + + public long FileSize { get; } + + public AssetFile(string fileName, string mimeType, long fileSize, Func openAction) + { + Guard.NotNullOrEmpty(fileName); + Guard.NotNullOrEmpty(mimeType); + Guard.GreaterEquals(fileSize, 0); + + FileName = fileName; + FileSize = fileSize; + + MimeType = mimeType; + + this.openAction = openAction; + } + + public Stream OpenRead() + { + return openAction(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs b/backend/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs new file mode 100644 index 000000000..7883a56b9 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.Serialization; + +namespace Squidex.Infrastructure.Assets +{ + [Serializable] + public class AssetNotFoundException : Exception + { + public AssetNotFoundException(string fileName) + : base(FormatMessage(fileName)) + { + } + + public AssetNotFoundException(string fileName, Exception inner) + : base(FormatMessage(fileName), inner) + { + } + + protected AssetNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + private static string FormatMessage(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + return $"An asset with name '{fileName}' does not exist."; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/AssetStoreExtensions.cs b/backend/src/Squidex.Infrastructure/Assets/AssetStoreExtensions.cs new file mode 100644 index 000000000..00fae3d2d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/AssetStoreExtensions.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public static class AssetStoreExtensions + { + public static string? GeneratePublicUrl(this IAssetStore store, Guid id, long version, string? suffix) + { + return store.GeneratePublicUrl(id.ToString(), version, suffix); + } + + public static string? GeneratePublicUrl(this IAssetStore store, string id, long version, string? suffix) + { + return store.GeneratePublicUrl(GetFileName(id, version, suffix)); + } + + public static Task CopyAsync(this IAssetStore store, string sourceFileName, Guid id, long version, string? suffix, CancellationToken ct = default) + { + return store.CopyAsync(sourceFileName, id.ToString(), version, suffix, ct); + } + + public static Task CopyAsync(this IAssetStore store, string sourceFileName, string id, long version, string? suffix, CancellationToken ct = default) + { + return store.CopyAsync(sourceFileName, GetFileName(id, version, suffix), ct); + } + + public static Task DownloadAsync(this IAssetStore store, Guid id, long version, string? suffix, Stream stream, CancellationToken ct = default) + { + return store.DownloadAsync(id.ToString(), version, suffix, stream, ct); + } + + public static Task DownloadAsync(this IAssetStore store, string id, long version, string? suffix, Stream stream, CancellationToken ct = default) + { + return store.DownloadAsync(GetFileName(id, version, suffix), stream, ct); + } + + public static Task UploadAsync(this IAssetStore store, Guid id, long version, string? suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + return store.UploadAsync(id.ToString(), version, suffix, stream, overwrite, ct); + } + + public static Task UploadAsync(this IAssetStore store, string id, long version, string? suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + return store.UploadAsync(GetFileName(id, version, suffix), stream, overwrite, ct); + } + + public static Task DeleteAsync(this IAssetStore store, Guid id, long version, string? suffix) + { + return store.DeleteAsync(id.ToString(), version, suffix); + } + + public static Task DeleteAsync(this IAssetStore store, string id, long version, string? suffix) + { + return store.DeleteAsync(GetFileName(id, version, suffix)); + } + + public static string GetFileName(string id, long version, string? suffix = null) + { + Guard.NotNullOrEmpty(id); + + return StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs new file mode 100644 index 000000000..a34f0c334 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs @@ -0,0 +1,158 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using FluentFTP; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class FTPAssetStore : IAssetStore, IInitializable + { + private readonly string path; + private readonly ISemanticLog log; + private readonly Func factory; + + public FTPAssetStore(Func factory, string path, ISemanticLog log) + { + Guard.NotNull(factory); + Guard.NotNullOrEmpty(path); + Guard.NotNull(log); + + this.factory = factory; + this.path = path; + + this.log = log; + } + + public string? GeneratePublicUrl(string fileName) + { + return null; + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + using (var client = factory()) + { + await client.ConnectAsync(ct); + + if (!await client.DirectoryExistsAsync(path, ct)) + { + await client.CreateDirectoryAsync(path, ct); + } + } + + log.LogInformation(w => w + .WriteProperty("action", "FTPAssetStoreConfigured") + .WriteProperty("path", path)); + } + + public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(sourceFileName); + Guard.NotNullOrEmpty(targetFileName); + + using (var client = GetFtpClient()) + { + var tempPath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + + using (var stream = new FileStream(tempPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose)) + { + await DownloadAsync(client, sourceFileName, stream, ct); + await UploadAsync(client, targetFileName, stream, false, ct); + } + } + } + + public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + Guard.NotNull(stream); + + using (var client = GetFtpClient()) + { + await DownloadAsync(client, fileName, stream, ct); + } + } + + public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + Guard.NotNull(stream); + + using (var client = GetFtpClient()) + { + await UploadAsync(client, fileName, stream, overwrite, ct); + } + } + + private static async Task DownloadAsync(IFtpClient client, string fileName, Stream stream, CancellationToken ct) + { + try + { + await client.DownloadAsync(stream, fileName, token: ct); + } + catch (FtpException ex) when (IsNotFound(ex)) + { + throw new AssetNotFoundException(fileName, ex); + } + } + + private static async Task UploadAsync(IFtpClient client, string fileName, Stream stream, bool overwrite, CancellationToken ct) + { + if (!overwrite && await client.FileExistsAsync(fileName, ct)) + { + throw new AssetAlreadyExistsException(fileName); + } + + await client.UploadAsync(stream, fileName, overwrite ? FtpExists.Overwrite : FtpExists.Skip, true, null, ct); + } + + public async Task DeleteAsync(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + using (var client = GetFtpClient()) + { + try + { + await client.DeleteFileAsync(fileName); + } + catch (FtpException ex) + { + if (!IsNotFound(ex)) + { + throw; + } + } + } + } + + private IFtpClient GetFtpClient() + { + var client = factory(); + + client.Connect(); + client.SetWorkingDirectory(path); + + return client; + } + + private static bool IsNotFound(Exception exception) + { + if (exception is FtpCommandException command) + { + return command.CompletionCode == "550"; + } + + return exception.InnerException != null && IsNotFound(exception.InnerException); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs new file mode 100644 index 000000000..95d8ebdc6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class FolderAssetStore : IAssetStore, IInitializable + { + private const int BufferSize = 81920; + private readonly ISemanticLog log; + private readonly DirectoryInfo directory; + + public FolderAssetStore(string path, ISemanticLog log) + { + Guard.NotNullOrEmpty(path); + Guard.NotNull(log); + + this.log = log; + + directory = new DirectoryInfo(path); + } + + public Task InitializeAsync(CancellationToken ct = default) + { + try + { + if (!directory.Exists) + { + directory.Create(); + } + + log.LogInformation(w => w + .WriteProperty("action", "FolderAssetStoreConfigured") + .WriteProperty("path", directory.FullName)); + + return TaskHelper.Done; + } + catch (Exception ex) + { + throw new ConfigurationException($"Cannot access directory {directory.FullName}", ex); + } + } + + public string? GeneratePublicUrl(string fileName) + { + return null; + } + + public Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(sourceFileName); + Guard.NotNullOrEmpty(targetFileName); + + var targetFile = GetFile(targetFileName); + var sourceFile = GetFile(sourceFileName); + + try + { + sourceFile.CopyTo(targetFile.FullName); + + return TaskHelper.Done; + } + catch (IOException) when (targetFile.Exists) + { + throw new AssetAlreadyExistsException(targetFileName); + } + catch (FileNotFoundException ex) + { + throw new AssetNotFoundException(sourceFileName, ex); + } + } + + public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + Guard.NotNull(stream); + + var file = GetFile(fileName); + + try + { + using (var fileStream = file.OpenRead()) + { + await fileStream.CopyToAsync(stream, BufferSize, ct); + } + } + catch (FileNotFoundException ex) + { + throw new AssetNotFoundException(fileName, ex); + } + } + + public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + Guard.NotNull(stream); + + var file = GetFile(fileName); + + try + { + using (var fileStream = file.Open(overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.Write)) + { + await stream.CopyToAsync(fileStream, BufferSize, ct); + } + } + catch (IOException) when (file.Exists) + { + throw new AssetAlreadyExistsException(file.Name); + } + } + + public Task DeleteAsync(string fileName) + { + var file = GetFile(fileName); + + file.Delete(); + + return TaskHelper.Done; + } + + private FileInfo GetFile(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + return new FileInfo(GetPath(fileName)); + } + + private string GetPath(string name) + { + return Path.Combine(directory.FullName, name); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/HasherStream.cs b/backend/src/Squidex.Infrastructure/Assets/HasherStream.cs new file mode 100644 index 000000000..314466fa7 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/HasherStream.cs @@ -0,0 +1,96 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class HasherStream : Stream + { + private readonly Stream inner; + private readonly IncrementalHash hasher; + + public override bool CanRead + { + get { return inner.CanRead; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return inner.Length; } + } + + public override long Position + { + get { return inner.Position; } + set { throw new NotSupportedException(); } + } + + public HasherStream(Stream inner, HashAlgorithmName hashAlgorithmName) + { + Guard.NotNull(inner); + + this.inner = inner; + + hasher = IncrementalHash.CreateHash(hashAlgorithmName); + } + + public override int Read(byte[] buffer, int offset, int count) + { + var read = inner.Read(buffer, offset, count); + + if (read > 0) + { + hasher.AppendData(buffer, offset, read); + } + + return read; + } + + public byte[] GetHashAndReset() + { + return hasher.GetHashAndReset(); + } + + public string GetHashStringAndReset() + { + return Convert.ToBase64String(GetHashAndReset()); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/IAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/IAssetStore.cs new file mode 100644 index 000000000..d00e89e65 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/IAssetStore.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public interface IAssetStore + { + string? GeneratePublicUrl(string fileName); + + Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default); + + Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default); + + Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default); + + Task DeleteAsync(string fileName); + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs b/backend/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs new file mode 100644 index 000000000..7358dc7e4 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public interface IAssetThumbnailGenerator + { + Task GetImageInfoAsync(Stream source); + + Task CreateThumbnailAsync(Stream source, Stream destination, int? width = null, int? height = null, string? mode = null, int? quality = null); + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs b/backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs new file mode 100644 index 000000000..707aac23b --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Assets +{ + public sealed class ImageInfo + { + public int PixelWidth { get; } + + public int PixelHeight { get; } + + public ImageInfo(int pixelWidth, int pixelHeight) + { + Guard.GreaterThan(pixelWidth, 0); + Guard.GreaterThan(pixelHeight, 0); + + PixelWidth = pixelWidth; + PixelHeight = pixelHeight; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs b/backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs new file mode 100644 index 000000000..57063172e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs @@ -0,0 +1,95 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Transforms; +using SixLabors.Primitives; + +namespace Squidex.Infrastructure.Assets.ImageSharp +{ + public sealed class ImageSharpAssetThumbnailGenerator : IAssetThumbnailGenerator + { + public Task CreateThumbnailAsync(Stream source, Stream destination, int? width = null, int? height = null, string? mode = null, int? quality = null) + { + return Task.Run(() => + { + if (!width.HasValue && !height.HasValue && !quality.HasValue) + { + source.CopyTo(destination); + + return; + } + + using (var sourceImage = Image.Load(source, out var format)) + { + var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format); + + if (quality.HasValue) + { + encoder = new JpegEncoder { Quality = quality.Value }; + } + + if (encoder == null) + { + throw new NotSupportedException(); + } + + if (width.HasValue || height.HasValue) + { + var isCropUpsize = string.Equals("CropUpsize", mode, StringComparison.OrdinalIgnoreCase); + + if (!Enum.TryParse(mode, true, out var resizeMode)) + { + resizeMode = ResizeMode.Max; + } + + if (isCropUpsize) + { + resizeMode = ResizeMode.Crop; + } + + var resizeWidth = width ?? 0; + var resizeHeight = height ?? 0; + + if (resizeWidth >= sourceImage.Width && resizeHeight >= sourceImage.Height && resizeMode == ResizeMode.Crop && !isCropUpsize) + { + resizeMode = ResizeMode.BoxPad; + } + + var options = new ResizeOptions { Size = new Size(resizeWidth, resizeHeight), Mode = resizeMode }; + + sourceImage.Mutate(x => x.Resize(options)); + } + + sourceImage.Save(destination, encoder); + } + }); + } + + public Task GetImageInfoAsync(Stream source) + { + return Task.Run(() => + { + try + { + var image = Image.Load(source); + + return new ImageInfo(image.Width, image.Height); + } + catch + { + return null; + } + }); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs new file mode 100644 index 000000000..5e6feec8f --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Concurrent; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public class MemoryAssetStore : IAssetStore + { + private readonly ConcurrentDictionary streams = new ConcurrentDictionary(); + private readonly AsyncLock readerLock = new AsyncLock(); + private readonly AsyncLock writerLock = new AsyncLock(); + + public string? GeneratePublicUrl(string fileName) + { + return null; + } + + public virtual async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(sourceFileName); + Guard.NotNullOrEmpty(targetFileName); + + if (!streams.TryGetValue(sourceFileName, out var sourceStream)) + { + throw new AssetNotFoundException(sourceFileName); + } + + using (await readerLock.LockAsync()) + { + await UploadAsync(targetFileName, sourceStream, false, ct); + } + } + + public virtual async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + Guard.NotNull(stream); + + if (!streams.TryGetValue(fileName, out var sourceStream)) + { + throw new AssetNotFoundException(fileName); + } + + using (await readerLock.LockAsync()) + { + try + { + await sourceStream.CopyToAsync(stream, 81920, ct); + } + finally + { + sourceStream.Position = 0; + } + } + } + + public virtual async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + Guard.NotNull(stream); + + var memoryStream = new MemoryStream(); + + async Task CopyAsync() + { + using (await writerLock.LockAsync()) + { + try + { + await stream.CopyToAsync(memoryStream, 81920, ct); + } + finally + { + memoryStream.Position = 0; + } + } + } + + if (overwrite) + { + await CopyAsync(); + + streams[fileName] = memoryStream; + } + else if (streams.TryAdd(fileName, memoryStream)) + { + await CopyAsync(); + } + else + { + throw new AssetAlreadyExistsException(fileName); + } + } + + public virtual Task DeleteAsync(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + streams.TryRemove(fileName, out _); + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs new file mode 100644 index 000000000..7f72920a8 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class NoopAssetStore : IAssetStore + { + public string? GeneratePublicUrl(string fileName) + { + return null; + } + + public Task CopyAsync(string sourceFileName, string fileName, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + + public Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + + public Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + + public Task DeleteAsync(string fileName) + { + throw new NotSupportedException(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs b/backend/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs new file mode 100644 index 000000000..9a7611c10 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Threading; +using Squidex.Infrastructure.Tasks; + +#pragma warning disable CS8601 // Possible null reference assignment. + +namespace Squidex.Infrastructure.Caching +{ + public sealed class AsyncLocalCache : ILocalCache + { + private static readonly AsyncLocal> LocalCache = new AsyncLocal>(); + private static readonly AsyncLocalCleaner> Cleaner; + + static AsyncLocalCache() + { + Cleaner = new AsyncLocalCleaner>(LocalCache); + } + + public IDisposable StartContext() + { + LocalCache.Value = new ConcurrentDictionary(); + + return Cleaner; + } + + public void Add(object key, object? value) + { + var cacheKey = GetCacheKey(key); + + var cache = LocalCache.Value; + + if (cache != null) + { + cache[cacheKey] = value; + } + } + + public void Remove(object key) + { + var cacheKey = GetCacheKey(key); + + var cache = LocalCache.Value; + + if (cache != null) + { + cache.TryRemove(cacheKey, out _); + } + } + + public bool TryGetValue(object key, out object? value) + { + var cacheKey = GetCacheKey(key); + + var cache = LocalCache.Value; + + if (cache != null) + { + return cache.TryGetValue(cacheKey, out value); + } + + value = null; + + return false; + } + + private static string GetCacheKey(object key) + { + return $"CACHE_{key}"; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs b/backend/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs new file mode 100644 index 000000000..b1248a6a2 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Caching.Memory; + +namespace Squidex.Infrastructure.Caching +{ + public abstract class CachingProviderBase + { + private readonly IMemoryCache cache; + + protected IMemoryCache Cache + { + get { return cache; } + } + + protected CachingProviderBase(IMemoryCache cache) + { + Guard.NotNull(cache); + + this.cache = cache; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Caching/ILocalCache.cs b/backend/src/Squidex.Infrastructure/Caching/ILocalCache.cs new file mode 100644 index 000000000..8be615ca3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Caching/ILocalCache.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Caching +{ + public interface ILocalCache + { + IDisposable StartContext(); + + void Add(object key, object? value); + + void Remove(object key); + + bool TryGetValue(object key, out object? value); + } +} diff --git a/backend/src/Squidex.Infrastructure/Caching/LRUCache.cs b/backend/src/Squidex.Infrastructure/Caching/LRUCache.cs new file mode 100644 index 000000000..29f4d8845 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Caching/LRUCache.cs @@ -0,0 +1,106 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Caching +{ + public sealed class LRUCache where TKey : notnull + { + private readonly Dictionary>> cacheMap = new Dictionary>>(); + private readonly LinkedList> cacheHistory = new LinkedList>(); + private readonly int capacity; + private readonly Action itemEvicted; + + public LRUCache(int capacity, Action? itemEvicted = null) + { + Guard.GreaterThan(capacity, 0); + + this.capacity = capacity; + + this.itemEvicted = itemEvicted ?? ((key, value) => { }); + } + + public bool Set(TKey key, TValue value) + { + if (cacheMap.TryGetValue(key, out var node)) + { + node.Value.Value = value; + + cacheHistory.Remove(node); + cacheHistory.AddLast(node); + + cacheMap[key] = node; + + return true; + } + + if (cacheMap.Count >= capacity) + { + RemoveFirst(); + } + + var cacheItem = new LRUCacheItem { Key = key, Value = value }; + + node = new LinkedListNode>(cacheItem); + + cacheMap.Add(key, node); + cacheHistory.AddLast(node); + + return false; + } + + public bool Remove(TKey key) + { + if (cacheMap.TryGetValue(key, out var node)) + { + cacheMap.Remove(key); + cacheHistory.Remove(node); + + return true; + } + + return false; + } + + public bool TryGetValue(TKey key, out object? value) + { + value = null; + + if (cacheMap.TryGetValue(key, out var node)) + { + value = node.Value.Value; + + cacheHistory.Remove(node); + cacheHistory.AddLast(node); + + return true; + } + + return false; + } + + public bool Contains(TKey key) + { + return cacheMap.ContainsKey(key); + } + + private void RemoveFirst() + { + var node = cacheHistory.First; + + if (node != null) + { + itemEvicted(node.Value.Key, node.Value.Value); + + cacheMap.Remove(node.Value.Key); + cacheHistory.RemoveFirst(); + } + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs b/backend/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs new file mode 100644 index 000000000..3a6b20ef4 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +#pragma warning disable SA1401 // Fields must be private +#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + +namespace Squidex.Infrastructure.Caching +{ + internal class LRUCacheItem + { + public TKey Key; + + public TValue Value; + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs b/backend/src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs rename to backend/src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs diff --git a/src/Squidex.Infrastructure/Cloneable.cs b/backend/src/Squidex.Infrastructure/Cloneable.cs similarity index 100% rename from src/Squidex.Infrastructure/Cloneable.cs rename to backend/src/Squidex.Infrastructure/Cloneable.cs diff --git a/src/Squidex.Infrastructure/Cloneable{T}.cs b/backend/src/Squidex.Infrastructure/Cloneable{T}.cs similarity index 100% rename from src/Squidex.Infrastructure/Cloneable{T}.cs rename to backend/src/Squidex.Infrastructure/Cloneable{T}.cs diff --git a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs new file mode 100644 index 000000000..379a65969 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -0,0 +1,234 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Squidex.Infrastructure +{ + public static class CollectionExtensions + { + public static IResultList SortSet(this IResultList input, Func idProvider, IReadOnlyList ids) where T : class + { + return ResultList.Create(input.Total, SortList(input, idProvider, ids)); + } + + public static IEnumerable SortList(this IEnumerable input, Func idProvider, IReadOnlyList ids) where T : class + { + return ids.Select(id => input.FirstOrDefault(x => Equals(idProvider(x), id))).Where(x => x != null); + } + + public static void AddRange(this ICollection target, IEnumerable source) + { + foreach (var value in source) + { + target.Add(value); + } + } + + public static IEnumerable Shuffle(this IEnumerable enumerable) + { + var random = new Random(); + + return enumerable.OrderBy(x => random.Next()).ToList(); + } + + public static IEnumerable OrEmpty(this IEnumerable? source) + { + return source ?? Enumerable.Empty(); + } + + public static IEnumerable Concat(this IEnumerable source, T value) + { + return source.Concat(Enumerable.Repeat(value, 1)); + } + + public static TResult[] Map(this T[] value, Func convert) + { + var result = new TResult[value.Length]; + + for (var i = 0; i < value.Length; i++) + { + result[i] = convert(value[i]); + } + + return result; + } + + public static int SequentialHashCode(this IEnumerable collection) + { + return collection.SequentialHashCode(EqualityComparer.Default); + } + + public static int SequentialHashCode(this IEnumerable collection, IEqualityComparer comparer) + { + var hashCode = 17; + + foreach (var item in collection) + { + if (!Equals(item, null)) + { + hashCode = (hashCode * 23) + comparer.GetHashCode(item); + } + } + + return hashCode; + } + + public static int OrderedHashCode(this IEnumerable collection) where T : notnull + { + return collection.OrderedHashCode(EqualityComparer.Default); + } + + public static int OrderedHashCode(this IEnumerable collection, IEqualityComparer comparer) where T : notnull + { + Guard.NotNull(comparer); + + var hashCodes = collection.Where(x => !Equals(x, null)).Select(x => x.GetHashCode()).OrderBy(x => x).ToArray(); + + var hashCode = 17; + + foreach (var code in hashCodes) + { + hashCode = (hashCode * 23) + code; + } + + return hashCode; + } + + public static int DictionaryHashCode(this IDictionary dictionary) where TKey : notnull + { + return DictionaryHashCode(dictionary, EqualityComparer.Default, EqualityComparer.Default); + } + + public static int DictionaryHashCode(this IDictionary dictionary, IEqualityComparer keyComparer, IEqualityComparer valueComparer) where TKey : notnull + { + var hashCode = 17; + + foreach (var kvp in dictionary.OrderBy(x => x.Key)) + { + hashCode = (hashCode * 23) + keyComparer.GetHashCode(kvp.Key); + + if (!Equals(kvp.Value, null)) + { + hashCode = (hashCode * 23) + valueComparer.GetHashCode(kvp.Value); + } + } + + return hashCode; + } + + public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary other) where TKey : notnull + { + return EqualsDictionary(dictionary, other, EqualityComparer.Default, EqualityComparer.Default); + } + + public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary other, IEqualityComparer keyComparer, IEqualityComparer valueComparer) where TKey : notnull + { + var comparer = new KeyValuePairComparer(keyComparer, valueComparer); + + return other != null && dictionary.Count == other.Count && !dictionary.Except(other, comparer).Any(); + } + + public static TValue GetOrDefault(this IReadOnlyDictionary dictionary, TKey key) where TKey : notnull + { + return dictionary.GetOrCreate(key, _ => default!); + } + + public static TValue GetOrAddDefault(this IDictionary dictionary, TKey key) where TKey : notnull + { + return dictionary.GetOrAdd(key, _ => default!); + } + + public static TValue GetOrNew(this IReadOnlyDictionary dictionary, TKey key) where TKey : notnull where TValue : class, new() + { + return dictionary.GetOrCreate(key, _ => new TValue()); + } + + public static TValue GetOrAddNew(this IDictionary dictionary, TKey key) where TKey : notnull where TValue : class, new() + { + return dictionary.GetOrAdd(key, _ => new TValue()); + } + + public static TValue GetOrCreate(this IReadOnlyDictionary dictionary, TKey key, Func creator) where TKey : notnull + { + if (!dictionary.TryGetValue(key, out var result)) + { + result = creator(key); + } + + return result; + } + + public static TValue GetOrAdd(this IDictionary dictionary, TKey key, TValue fallback) where TKey : notnull + { + if (!dictionary.TryGetValue(key, out var result)) + { + result = fallback; + + dictionary.Add(key, result); + } + + return result; + } + + public static TValue GetOrAdd(this IDictionary dictionary, TKey key, Func creator) where TKey : notnull + { + if (!dictionary.TryGetValue(key, out var result)) + { + result = creator(key); + + dictionary.Add(key, result); + } + + return result; + } + + public static TValue GetOrAdd(this IDictionary dictionary, TKey key, TContext context, Func creator) where TKey : notnull + { + if (!dictionary.TryGetValue(key, out var result)) + { + result = creator(key, context); + + dictionary.Add(key, result); + } + + return result; + } + + public static void Foreach(this IEnumerable collection, Action action) + { + foreach (var item in collection) + { + action(item); + } + } + + public sealed class KeyValuePairComparer : IEqualityComparer> + { + private readonly IEqualityComparer keyComparer; + private readonly IEqualityComparer valueComparer; + + public KeyValuePairComparer(IEqualityComparer keyComparer, IEqualityComparer valueComparer) + { + this.keyComparer = keyComparer; + this.valueComparer = valueComparer; + } + + public bool Equals(KeyValuePair x, KeyValuePair y) + { + return keyComparer.Equals(x.Key, y.Key) && valueComparer.Equals(x.Value, y.Value); + } + + public int GetHashCode(KeyValuePair obj) + { + return keyComparer.GetHashCode(obj.Key) ^ valueComparer.GetHashCode(obj.Value); + } + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary.cs b/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary.cs new file mode 100644 index 000000000..0ac20b261 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Squidex.Infrastructure.Collections +{ + public static class ArrayDictionary + { + public static ArrayDictionary ToArrayDictionary(this IEnumerable source, Func keyExtractor) where TKey : notnull + { + return new ArrayDictionary(source.Select(x => new KeyValuePair(keyExtractor(x), x)).ToArray()); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs b/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs new file mode 100644 index 000000000..86a81a006 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs @@ -0,0 +1,165 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Squidex.Infrastructure.Collections +{ + public class ArrayDictionary : IReadOnlyDictionary where TKey : notnull + { + private readonly IEqualityComparer keyComparer; + private readonly KeyValuePair[] items; + + public TValue this[TKey key] + { + get + { + if (!TryGetValue(key, out var value)) + { + throw new KeyNotFoundException(); + } + + return value; + } + } + + public IEnumerable Keys + { + get { return items.Select(x => x.Key); } + } + + public IEnumerable Values + { + get { return items.Select(x => x.Value); } + } + + public int Count + { + get { return items.Length; } + } + + public ArrayDictionary() + : this(EqualityComparer.Default, Array.Empty>()) + { + } + + public ArrayDictionary(KeyValuePair[] items) + : this(EqualityComparer.Default, items) + { + } + + public ArrayDictionary(IEqualityComparer keyComparer, KeyValuePair[] items) + { + Guard.NotNull(items, nameof(items)); + Guard.NotNull(keyComparer, nameof(keyComparer)); + + this.items = items; + + this.keyComparer = keyComparer; + } + + public KeyValuePair[] With(TKey key, TValue value) + { + var result = new List>(Math.Max(items.Length, 1)); + + var wasReplaced = false; + + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + + if (wasReplaced || !keyComparer.Equals(item.Key, key)) + { + result.Add(item); + } + else + { + result.Add(new KeyValuePair(key, value)); + wasReplaced = true; + } + } + + if (!wasReplaced) + { + result.Add(new KeyValuePair(key, value)); + } + + return result.ToArray(); + } + + public KeyValuePair[] Without(TKey key) + { + var result = new List>(Math.Max(items.Length, 1)); + + var wasRemoved = false; + + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + + if (wasRemoved || !keyComparer.Equals(item.Key, key)) + { + result.Add(item); + } + else + { + wasRemoved = true; + } + } + + return result.ToArray(); + } + + public bool ContainsKey(TKey key) + { + for (var i = 0; i < items.Length; i++) + { + if (keyComparer.Equals(items[i].Key, key)) + { + return true; + } + } + + return false; + } + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + for (var i = 0; i < items.Length; i++) + { + if (keyComparer.Equals(items[i].Key, key)) + { + value = items[i].Value; + return true; + } + } + + value = default!; + + return false; + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetEnumerable(items).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return items.GetEnumerator(); + } + + private static IEnumerable GetEnumerable(IEnumerable array) + { + return array; + } + } +} diff --git a/src/Squidex.Infrastructure/Collections/ReadOnlyCollection.cs b/backend/src/Squidex.Infrastructure/Collections/ReadOnlyCollection.cs similarity index 100% rename from src/Squidex.Infrastructure/Collections/ReadOnlyCollection.cs rename to backend/src/Squidex.Infrastructure/Collections/ReadOnlyCollection.cs diff --git a/backend/src/Squidex.Infrastructure/Commands/CommandContext.cs b/backend/src/Squidex.Infrastructure/Commands/CommandContext.cs new file mode 100644 index 000000000..ccc82b13c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/CommandContext.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Commands +{ + public sealed class CommandContext + { + private Tuple? result; + + public Guid ContextId { get; } = Guid.NewGuid(); + + public ICommand Command { get; } + + public ICommandBus CommandBus { get; } + + public object? PlainResult + { + get { return result?.Item1; } + } + + public bool IsCompleted + { + get { return result != null; } + } + + public CommandContext(ICommand command, ICommandBus commandBus) + { + Guard.NotNull(command); + Guard.NotNull(commandBus); + + Command = command; + CommandBus = commandBus; + } + + public CommandContext Complete(object? resultValue = null) + { + result = Tuple.Create(resultValue); + + return this; + } + + public T Result() + { + return (T)result?.Item1!; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs b/backend/src/Squidex.Infrastructure/Commands/CommandExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/CommandExtensions.cs rename to backend/src/Squidex.Infrastructure/Commands/CommandExtensions.cs diff --git a/backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs b/backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs new file mode 100644 index 000000000..cbb7f4910 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Commands +{ + public sealed class CustomCommandMiddlewareRunner : ICommandMiddleware + { + private readonly IEnumerable extensions; + + public CustomCommandMiddlewareRunner(IEnumerable extensions) + { + Guard.NotNull(extensions); + + this.extensions = extensions.Reverse().ToList(); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + foreach (var handler in extensions) + { + next = Join(handler, context, next); + } + + await next(); + } + + private static Func Join(ICommandMiddleware handler, CommandContext context, Func next) + { + return () => handler.HandleAsync(context, next); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs new file mode 100644 index 000000000..ea63d15da --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.Commands +{ + public abstract class DomainObjectGrain : DomainObjectGrainBase where T : class, IDomainState, new() + { + private readonly IStore store; + private T snapshot = new T { Version = EtagVersion.Empty }; + private IPersistence? persistence; + + public override T Snapshot + { + get { return snapshot; } + } + + protected DomainObjectGrain(IStore store, ISemanticLog log) + : base(log) + { + Guard.NotNull(store); + + this.store = store; + } + + protected sealed override void ApplyEvent(Envelope @event) + { + var newVersion = Version + 1; + + snapshot = OnEvent(@event); + snapshot.Version = newVersion; + } + + protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) + { + snapshot = previousSnapshot; + } + + protected sealed override Task ReadAsync(Type type, Guid id) + { + persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, new HandleSnapshot(ApplySnapshot), ApplyEvent); + + return persistence.ReadAsync(); + } + + private void ApplySnapshot(T state) + { + snapshot = state; + } + + protected sealed override async Task WriteAsync(Envelope[] events, long previousVersion) + { + if (events.Length > 0 && persistence != null) + { + await persistence.WriteEventsAsync(events); + await persistence.WriteSnapshotAsync(Snapshot); + } + } + + protected T OnEvent(Envelope @event) + { + return Snapshot.Apply(@event); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs new file mode 100644 index 000000000..65358a154 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs @@ -0,0 +1,226 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Commands +{ + public abstract class DomainObjectGrainBase : GrainOfGuid, IDomainObjectGrain where T : IDomainState, new() + { + private readonly List> uncomittedEvents = new List>(); + private readonly ISemanticLog log; + private Guid id; + + private enum Mode + { + Create, + Update, + Upsert + } + + public Guid Id + { + get { return id; } + } + + public long Version + { + get { return Snapshot.Version; } + } + + public abstract T Snapshot { get; } + + protected DomainObjectGrainBase(ISemanticLog log) + { + Guard.NotNull(log); + + this.log = log; + } + + protected override async Task OnActivateAsync(Guid key) + { + var logContext = (key: key.ToString(), name: GetType().Name); + + using (log.MeasureInformation(logContext, (ctx, w) => w + .WriteProperty("action", "ActivateDomainObject") + .WriteProperty("domainObjectType", ctx.name) + .WriteProperty("domainObjectKey", ctx.key))) + { + id = key; + + await ReadAsync(GetType(), id); + } + } + + public void RaiseEvent(IEvent @event) + { + RaiseEvent(Envelope.Create(@event)); + } + + public virtual void RaiseEvent(Envelope @event) + { + Guard.NotNull(@event); + + @event.SetAggregateId(id); + + ApplyEvent(@event); + + uncomittedEvents.Add(@event); + } + + public IReadOnlyList> GetUncomittedEvents() + { + return uncomittedEvents; + } + + public void ClearUncommittedEvents() + { + uncomittedEvents.Clear(); + } + + protected Task CreateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler, Mode.Create); + } + + protected Task CreateReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToAsync()!, Mode.Create); + } + + protected Task CreateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler.ToDefault(), Mode.Create); + } + + protected Task Create(TCommand command, Action handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, Mode.Create); + } + + protected Task UpdateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler, Mode.Update); + } + + protected Task UpdateReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToAsync()!, Mode.Update); + } + + protected Task UpdateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToDefault()!, Mode.Update); + } + + protected Task Update(TCommand command, Action handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, Mode.Update); + } + + protected Task UpsertReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler, Mode.Upsert); + } + + protected Task UpsertReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToAsync()!, Mode.Upsert); + } + + protected Task UpsertAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToDefault()!, Mode.Upsert); + } + + protected Task Upsert(TCommand command, Action handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, Mode.Upsert); + } + + private async Task InvokeAsync(TCommand command, Func> handler, Mode mode) where TCommand : class, IAggregateCommand + { + Guard.NotNull(command); + Guard.NotNull(handler); + + if (command.ExpectedVersion > EtagVersion.Any && command.ExpectedVersion != Version) + { + throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion); + } + + if (mode == Mode.Update && Version < 0) + { + TryDeactivateOnIdle(); + + throw new DomainObjectNotFoundException(id.ToString(), GetType()); + } + + if (mode == Mode.Create && Version >= 0) + { + throw new DomainException("Object has already been created."); + } + + var previousSnapshot = Snapshot; + var previousVersion = Version; + try + { + var result = await handler(command); + + var events = uncomittedEvents.ToArray(); + + await WriteAsync(events, previousVersion); + + if (result == null) + { + if (mode == Mode.Update || (mode == Mode.Upsert && Version == 0)) + { + result = new EntitySavedResult(Version); + } + else + { + result = EntityCreatedResult.Create(id, Version); + } + } + + return result; + } + catch + { + RestorePreviousSnapshot(previousSnapshot, previousVersion); + + throw; + } + finally + { + ClearUncommittedEvents(); + } + } + + protected abstract void RestorePreviousSnapshot(T previousSnapshot, long previousVersion); + + protected abstract void ApplyEvent(Envelope @event); + + protected abstract Task ReadAsync(Type type, Guid id); + + protected abstract Task WriteAsync(Envelope[] events, long previousVersion); + + public async Task> ExecuteAsync(J command) + { + var result = await ExecuteAsync(command.Value); + + return result; + } + + protected abstract Task ExecuteAsync(IAggregateCommand command); + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs new file mode 100644 index 000000000..5963bdec0 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Orleans; + +namespace Squidex.Infrastructure.Commands +{ + public static class DomainObjectGrainFormatter + { + public static string Format(IGrainCallContext context) + { + if (context.InterfaceMethod == null) + { + return "Unknown"; + } + + if (string.Equals(context.InterfaceMethod.Name, nameof(IDomainObjectGrain.ExecuteAsync), StringComparison.CurrentCultureIgnoreCase) && + context.Arguments?.Length == 1 && + context.Arguments[0] != null) + { + var argumentFullName = context.Arguments[0].ToString(); + + if (argumentFullName != null) + { + var argumentParts = argumentFullName.Split('.'); + var argumentName = argumentParts[^1]; + + return $"{nameof(IDomainObjectGrain.ExecuteAsync)}({argumentName})"; + } + } + + return context.InterfaceMethod.Name; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs new file mode 100644 index 000000000..0d2ecd412 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using NodaTime; + +namespace Squidex.Infrastructure.Commands +{ + public sealed class EnrichWithTimestampCommandMiddleware : ICommandMiddleware + { + private readonly IClock clock; + + public EnrichWithTimestampCommandMiddleware(IClock clock) + { + Guard.NotNull(clock); + + this.clock = clock; + } + + public Task HandleAsync(CommandContext context, Func next) + { + if (context.Command is ITimestampCommand timestampCommand) + { + timestampCommand.Timestamp = clock.GetCurrentInstant(); + } + + return next(); + } + } +} diff --git a/src/Squidex.Infrastructure/Commands/EntityCreatedResult.cs b/backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/EntityCreatedResult.cs rename to backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult.cs diff --git a/src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs b/backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs rename to backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs diff --git a/src/Squidex.Infrastructure/Commands/EntitySavedResult.cs b/backend/src/Squidex.Infrastructure/Commands/EntitySavedResult.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/EntitySavedResult.cs rename to backend/src/Squidex.Infrastructure/Commands/EntitySavedResult.cs diff --git a/backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs new file mode 100644 index 000000000..0829900cd --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; + +namespace Squidex.Infrastructure.Commands +{ + public class GrainCommandMiddleware : ICommandMiddleware where TCommand : IAggregateCommand where TGrain : IDomainObjectGrain + { + private readonly IGrainFactory grainFactory; + + public GrainCommandMiddleware(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public virtual async Task HandleAsync(CommandContext context, Func next) + { + await ExecuteCommandAsync(context); + + await next(); + } + + protected async Task ExecuteCommandAsync(CommandContext context) + { + if (context.Command is TCommand typedCommand) + { + var result = await ExecuteCommandAsync(typedCommand); + + context.Complete(result); + } + } + + private async Task ExecuteCommandAsync(TCommand typedCommand) + { + var grain = grainFactory.GetGrain(typedCommand.AggregateId); + + var result = await grain.ExecuteAsync(typedCommand); + + return result.Value; + } + } +} diff --git a/src/Squidex.Infrastructure/Commands/IAggregateCommand.cs b/backend/src/Squidex.Infrastructure/Commands/IAggregateCommand.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/IAggregateCommand.cs rename to backend/src/Squidex.Infrastructure/Commands/IAggregateCommand.cs diff --git a/src/Squidex.Infrastructure/Commands/ICommand.cs b/backend/src/Squidex.Infrastructure/Commands/ICommand.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/ICommand.cs rename to backend/src/Squidex.Infrastructure/Commands/ICommand.cs diff --git a/src/Squidex.Infrastructure/Commands/ICommandBus.cs b/backend/src/Squidex.Infrastructure/Commands/ICommandBus.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/ICommandBus.cs rename to backend/src/Squidex.Infrastructure/Commands/ICommandBus.cs diff --git a/src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs rename to backend/src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs diff --git a/src/Squidex.Infrastructure/Commands/ICustomCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/ICustomCommandMiddleware.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/ICustomCommandMiddleware.cs rename to backend/src/Squidex.Infrastructure/Commands/ICustomCommandMiddleware.cs diff --git a/backend/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs b/backend/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs new file mode 100644 index 000000000..ea10c037b --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Infrastructure.Commands +{ + public interface IDomainObjectGrain : IGrainWithGuidKey + { + Task> ExecuteAsync(J command); + } +} diff --git a/src/Squidex.Infrastructure/Commands/IDomainState.cs b/backend/src/Squidex.Infrastructure/Commands/IDomainState.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/IDomainState.cs rename to backend/src/Squidex.Infrastructure/Commands/IDomainState.cs diff --git a/src/Squidex.Infrastructure/Commands/ITimestampCommand.cs b/backend/src/Squidex.Infrastructure/Commands/ITimestampCommand.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/ITimestampCommand.cs rename to backend/src/Squidex.Infrastructure/Commands/ITimestampCommand.cs diff --git a/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs b/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs new file mode 100644 index 000000000..987f81711 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Commands +{ + public sealed class InMemoryCommandBus : ICommandBus + { + private readonly List middlewares; + + public InMemoryCommandBus(IEnumerable middlewares) + { + Guard.NotNull(middlewares); + + this.middlewares = middlewares.Reverse().ToList(); + } + + public async Task PublishAsync(ICommand command) + { + Guard.NotNull(command); + + var context = new CommandContext(command, this); + + var next = new Func(() => TaskHelper.Done); + + foreach (var handler in middlewares) + { + next = Join(handler, context, next); + } + + await next(); + + return context; + } + + private static Func Join(ICommandMiddleware handler, CommandContext context, Func next) + { + return () => handler.HandleAsync(context, next); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs new file mode 100644 index 000000000..367cfdd4e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.Commands +{ + public sealed class LogCommandMiddleware : ICommandMiddleware + { + private readonly ISemanticLog log; + + public LogCommandMiddleware(ISemanticLog log) + { + Guard.NotNull(log); + + this.log = log; + } + + public async Task HandleAsync(CommandContext context, Func next) + { + var logContext = (id: context.ContextId.ToString(), command: context.Command.GetType().Name); + + try + { + log.LogInformation(logContext, (ctx, w) => w + .WriteProperty("action", "HandleCommand.") + .WriteProperty("actionId", ctx.id) + .WriteProperty("status", "Started") + .WriteProperty("commandType", ctx.command)); + + using (log.MeasureInformation(logContext, (ctx, w) => w + .WriteProperty("action", "HandleCommand.") + .WriteProperty("actionId", ctx.id) + .WriteProperty("status", "Completed") + .WriteProperty("commandType", ctx.command))) + { + await next(); + } + + log.LogInformation(logContext, (ctx, w) => w + .WriteProperty("action", "HandleCommand.") + .WriteProperty("actionId", ctx.id) + .WriteProperty("status", "Succeeded") + .WriteProperty("commandType", ctx.command)); + } + catch (Exception ex) + { + log.LogError(ex, logContext, (ctx, w) => w + .WriteProperty("action", "HandleCommand.") + .WriteProperty("actionId", ctx.id) + .WriteProperty("status", "Failed") + .WriteProperty("commandType", ctx.command)); + + throw; + } + + if (!context.IsCompleted) + { + log.LogFatal(logContext, (ctx, w) => w + .WriteProperty("action", "HandleCommand.") + .WriteProperty("actionId", ctx.id) + .WriteProperty("status", "Unhandled") + .WriteProperty("commandType", ctx.command)); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs b/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs new file mode 100644 index 000000000..bbfb4cd91 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs @@ -0,0 +1,96 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.Commands +{ + public abstract class LogSnapshotDomainObjectGrain : DomainObjectGrainBase where T : class, IDomainState, new() + { + private readonly IStore store; + private readonly List snapshots = new List { new T { Version = EtagVersion.Empty } }; + private IPersistence? persistence; + + public override T Snapshot + { + get { return snapshots.Last(); } + } + + protected LogSnapshotDomainObjectGrain(IStore store, ISemanticLog log) + : base(log) + { + Guard.NotNull(log); + + this.store = store; + } + + public T GetSnapshot(long version) + { + if (version == EtagVersion.Any || version == EtagVersion.Auto) + { + return Snapshot; + } + + if (version == EtagVersion.Empty) + { + return snapshots[0]; + } + + if (version >= 0 && version < snapshots.Count - 1) + { + return snapshots[(int)version + 1]; + } + + return default!; + } + + protected sealed override void ApplyEvent(Envelope @event) + { + var snapshot = OnEvent(@event); + + snapshot.Version = Version + 1; + snapshots.Add(snapshot); + } + + protected sealed override Task ReadAsync(Type type, Guid id) + { + persistence = store.WithEventSourcing(type, id, ApplyEvent); + + return persistence.ReadAsync(); + } + + protected sealed override async Task WriteAsync(Envelope[] events, long previousVersion) + { + if (events.Length > 0 && persistence != null) + { + var persistedSnapshots = store.GetSnapshotStore(); + + await persistence.WriteEventsAsync(events); + await persistedSnapshots.WriteAsync(Id, Snapshot, previousVersion, previousVersion + events.Length); + } + } + + protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) + { + while (snapshots.Count > previousVersion + 2) + { + snapshots.RemoveAt(snapshots.Count - 1); + } + } + + protected T OnEvent(Envelope @event) + { + return Snapshot.Apply(@event); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs new file mode 100644 index 000000000..81d2b54ec --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Squidex.Infrastructure.Commands +{ + public sealed class ReadonlyCommandMiddleware : ICommandMiddleware + { + private readonly ReadonlyOptions options; + + public ReadonlyCommandMiddleware(IOptions options) + { + Guard.NotNull(options); + + this.options = options.Value; + } + + public Task HandleAsync(CommandContext context, Func next) + { + if (options.IsReadonly) + { + throw new DomainException("Application is in readonly mode at the moment."); + } + + return next(); + } + } +} diff --git a/src/Squidex.Infrastructure/Commands/ReadonlyOptions.cs b/backend/src/Squidex.Infrastructure/Commands/ReadonlyOptions.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/ReadonlyOptions.cs rename to backend/src/Squidex.Infrastructure/Commands/ReadonlyOptions.cs diff --git a/src/Squidex.Infrastructure/Configuration/Alternatives.cs b/backend/src/Squidex.Infrastructure/Configuration/Alternatives.cs similarity index 100% rename from src/Squidex.Infrastructure/Configuration/Alternatives.cs rename to backend/src/Squidex.Infrastructure/Configuration/Alternatives.cs diff --git a/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs b/backend/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs rename to backend/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs diff --git a/src/Squidex.Infrastructure/ConfigurationException.cs b/backend/src/Squidex.Infrastructure/ConfigurationException.cs similarity index 100% rename from src/Squidex.Infrastructure/ConfigurationException.cs rename to backend/src/Squidex.Infrastructure/ConfigurationException.cs diff --git a/backend/src/Squidex.Infrastructure/DelegateDisposable.cs b/backend/src/Squidex.Infrastructure/DelegateDisposable.cs new file mode 100644 index 000000000..f24bd2913 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/DelegateDisposable.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure +{ + public sealed class DelegateDisposable : IDisposable + { + private readonly Action action; + + public DelegateDisposable(Action action) + { + Guard.NotNull(action); + + this.action = action; + } + + public void Dispose() + { + action(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs b/backend/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs new file mode 100644 index 000000000..e7fc8dad6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs @@ -0,0 +1,96 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Squidex.Infrastructure; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class DependencyInjectionExtensions + { + public delegate void Registrator(Type serviceType, Func implementationFactory); + + public sealed class InterfaceRegistrator where T : notnull + { + private readonly Registrator register; + private readonly Registrator registerOptional; + + public InterfaceRegistrator(Registrator register, Registrator registerOptional) + { + this.register = register; + this.registerOptional = registerOptional; + + var interfaces = typeof(T).GetInterfaces(); + + if (interfaces.Contains(typeof(IInitializable))) + { + register(typeof(IInitializable), c => c.GetRequiredService()); + } + + if (interfaces.Contains(typeof(IBackgroundProcess))) + { + register(typeof(IBackgroundProcess), c => c.GetRequiredService()); + } + } + + public InterfaceRegistrator AsSelf() + { + return this; + } + + public InterfaceRegistrator AsOptional() + { + if (typeof(TInterface) != typeof(T)) + { + registerOptional(typeof(TInterface), c => c.GetRequiredService()); + } + + return this; + } + + public InterfaceRegistrator As() + { + if (typeof(TInterface) != typeof(T)) + { + register(typeof(TInterface), c => c.GetRequiredService()); + } + + return this; + } + } + + public static InterfaceRegistrator AddTransientAs(this IServiceCollection services, Func factory) where T : class + { + services.AddTransient(typeof(T), factory); + + return new InterfaceRegistrator((t, f) => services.AddTransient(t, f), services.TryAddTransient); + } + + public static InterfaceRegistrator AddTransientAs(this IServiceCollection services) where T : class + { + services.AddTransient(); + + return new InterfaceRegistrator((t, f) => services.AddTransient(t, f), services.TryAddTransient); + } + + public static InterfaceRegistrator AddSingletonAs(this IServiceCollection services, Func factory) where T : class + { + services.AddSingleton(typeof(T), factory); + + return new InterfaceRegistrator((t, f) => services.AddSingleton(t, f), services.TryAddSingleton); + } + + public static InterfaceRegistrator AddSingletonAs(this IServiceCollection services) where T : class + { + services.AddSingleton(); + + return new InterfaceRegistrator((t, f) => services.AddSingleton(t, f), services.TryAddSingleton); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs b/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs new file mode 100644 index 000000000..1dcb2d582 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class GCHealthCheck : IHealthCheck + { + private readonly long threshold; + + public GCHealthCheck(IOptions options) + { + Guard.NotNull(options); + + threshold = 1024 * 1024 * options.Value.Threshold; + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var allocated = GC.GetTotalMemory(false); + + var data = new Dictionary + { + { "Allocated", allocated.ToReadableSize() }, + { "Gen0Collections", GC.CollectionCount(0) }, + { "Gen1Collections", GC.CollectionCount(1) }, + { "Gen2Collections", GC.CollectionCount(2) } + }; + + var status = allocated < threshold ? HealthStatus.Healthy : HealthStatus.Unhealthy; + + var message = $"Application must consume less than {threshold.ToReadableSize()} memory."; + + return Task.FromResult(new HealthCheckResult(status, message, data: data)); + } + } +} diff --git a/src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs b/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs similarity index 100% rename from src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs rename to backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs diff --git a/backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs b/backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs new file mode 100644 index 000000000..a2cf9e4f4 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Orleans; +using Orleans.Runtime; + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class OrleansHealthCheck : IHealthCheck + { + private readonly IManagementGrain managementGrain; + + public OrleansHealthCheck(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + managementGrain = grainFactory.GetGrain(0); + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var activationCount = await managementGrain.GetTotalActivationCount(); + + var status = activationCount > 0 ? + HealthStatus.Healthy : + HealthStatus.Unhealthy; + + return new HealthCheckResult(status, "Orleans must have at least one activation."); + } + } +} diff --git a/src/Squidex.Infrastructure/DisposableObjectBase.cs b/backend/src/Squidex.Infrastructure/DisposableObjectBase.cs similarity index 100% rename from src/Squidex.Infrastructure/DisposableObjectBase.cs rename to backend/src/Squidex.Infrastructure/DisposableObjectBase.cs diff --git a/backend/src/Squidex.Infrastructure/DomainException.cs b/backend/src/Squidex.Infrastructure/DomainException.cs new file mode 100644 index 000000000..666f0f65c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/DomainException.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.Serialization; + +namespace Squidex.Infrastructure +{ + [Serializable] + public class DomainException : Exception + { + public DomainException(string message) + : base(message) + { + } + + public DomainException(string message, Exception? inner) + : base(message, inner) + { + } + + protected DomainException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/Squidex.Infrastructure/DomainForbiddenException.cs b/backend/src/Squidex.Infrastructure/DomainForbiddenException.cs similarity index 100% rename from src/Squidex.Infrastructure/DomainForbiddenException.cs rename to backend/src/Squidex.Infrastructure/DomainForbiddenException.cs diff --git a/src/Squidex.Infrastructure/DomainObjectDeletedException.cs b/backend/src/Squidex.Infrastructure/DomainObjectDeletedException.cs similarity index 100% rename from src/Squidex.Infrastructure/DomainObjectDeletedException.cs rename to backend/src/Squidex.Infrastructure/DomainObjectDeletedException.cs diff --git a/backend/src/Squidex.Infrastructure/DomainObjectException.cs b/backend/src/Squidex.Infrastructure/DomainObjectException.cs new file mode 100644 index 000000000..4742bcc33 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/DomainObjectException.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.Serialization; + +namespace Squidex.Infrastructure +{ + [Serializable] + public class DomainObjectException : Exception + { + public string? TypeName { get; } + + public string Id { get; } + + protected DomainObjectException(string message, string id, Type type, Exception? inner = null) + : base(message, inner) + { + Id = id; + + TypeName = type?.Name; + } + + protected DomainObjectException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Id = info.GetString(nameof(Id))!; + + TypeName = info.GetString(nameof(TypeName))!; + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(Id), Id); + info.AddValue(nameof(TypeName), TypeName); + + base.GetObjectData(info, context); + } + } +} diff --git a/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs b/backend/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs similarity index 100% rename from src/Squidex.Infrastructure/DomainObjectNotFoundException.cs rename to backend/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs diff --git a/src/Squidex.Infrastructure/DomainObjectVersionException.cs b/backend/src/Squidex.Infrastructure/DomainObjectVersionException.cs similarity index 100% rename from src/Squidex.Infrastructure/DomainObjectVersionException.cs rename to backend/src/Squidex.Infrastructure/DomainObjectVersionException.cs diff --git a/src/Squidex.Infrastructure/Email/IEmailSender.cs b/backend/src/Squidex.Infrastructure/Email/IEmailSender.cs similarity index 100% rename from src/Squidex.Infrastructure/Email/IEmailSender.cs rename to backend/src/Squidex.Infrastructure/Email/IEmailSender.cs diff --git a/src/Squidex.Infrastructure/Email/SmptOptions.cs b/backend/src/Squidex.Infrastructure/Email/SmptOptions.cs similarity index 100% rename from src/Squidex.Infrastructure/Email/SmptOptions.cs rename to backend/src/Squidex.Infrastructure/Email/SmptOptions.cs diff --git a/backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs b/backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs new file mode 100644 index 000000000..347cc48ec --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Net; +using System.Net.Mail; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Squidex.Infrastructure.Email +{ + public sealed class SmtpEmailSender : IEmailSender + { + private readonly SmtpClient smtpClient; + private readonly string sender; + + public SmtpEmailSender(IOptions options) + { + Guard.NotNull(options); + + var config = options.Value; + + smtpClient = new SmtpClient(config.Server, config.Port) + { + Credentials = new NetworkCredential( + config.Username, + config.Password), + EnableSsl = config.EnableSsl + }; + + sender = config.Sender; + } + + public Task SendAsync(string recipient, string subject, string body) + { + return smtpClient.SendMailAsync(sender, recipient, subject, body); + } + } +} diff --git a/src/Squidex.Infrastructure/EtagVersion.cs b/backend/src/Squidex.Infrastructure/EtagVersion.cs similarity index 100% rename from src/Squidex.Infrastructure/EtagVersion.cs rename to backend/src/Squidex.Infrastructure/EtagVersion.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs b/backend/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/CompoundEventConsumer.cs b/backend/src/Squidex.Infrastructure/EventSourcing/CompoundEventConsumer.cs new file mode 100644 index 000000000..d594b2590 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/CompoundEventConsumer.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class CompoundEventConsumer : IEventConsumer + { + private readonly IEventConsumer[] inners; + + public string Name { get; } + + public string EventsFilter { get; } + + public CompoundEventConsumer(IEventConsumer first, params IEventConsumer[] inners) + : this(first?.Name!, first!, inners) + { + } + + public CompoundEventConsumer(IEventConsumer[] inners) + { + Guard.NotNull(inners); + Guard.NotEmpty(inners); + + this.inners = inners; + + Name = inners.First().Name; + + var innerFilters = + this.inners.Where(x => !string.IsNullOrWhiteSpace(x.EventsFilter)) + .Select(x => $"({x.EventsFilter})"); + + EventsFilter = string.Join("|", innerFilters); + } + + public CompoundEventConsumer(string name, IEventConsumer first, params IEventConsumer[] inners) + { + Guard.NotNull(first); + Guard.NotNull(inners); + Guard.NotNullOrEmpty(name); + + this.inners = new[] { first }.Union(inners).ToArray(); + + Name = name; + + var innerFilters = + this.inners.Where(x => !string.IsNullOrWhiteSpace(x.EventsFilter)) + .Select(x => $"({x.EventsFilter})"); + + EventsFilter = string.Join("|", innerFilters); + } + + public bool Handles(StoredEvent @event) + { + return inners.Any(x => x.Handles(@event)); + } + + public Task ClearAsync() + { + return Task.WhenAll(inners.Select(i => i.ClearAsync())); + } + + public async Task On(Envelope @event) + { + foreach (var inner in inners) + { + await inner.On(@event); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs new file mode 100644 index 000000000..2e93cac7a --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class DefaultEventDataFormatter : IEventDataFormatter + { + private readonly IJsonSerializer serializer; + private readonly TypeNameRegistry typeNameRegistry; + + public DefaultEventDataFormatter(TypeNameRegistry typeNameRegistry, IJsonSerializer serializer) + { + Guard.NotNull(typeNameRegistry); + Guard.NotNull(serializer); + + this.typeNameRegistry = typeNameRegistry; + + this.serializer = serializer; + } + + public Envelope Parse(EventData eventData, Func? stringConverter = null) + { + var payloadType = typeNameRegistry.GetType(eventData.Type); + var payloadObj = serializer.Deserialize(eventData.Payload, payloadType, stringConverter); + + if (payloadObj is IMigrated migratedEvent) + { + payloadObj = migratedEvent.Migrate(); + + if (ReferenceEquals(migratedEvent, payloadObj)) + { + Debug.WriteLine("Migration should return new event."); + } + } + + var envelope = new Envelope(payloadObj, eventData.Headers); + + return envelope; + } + + public EventData ToEventData(Envelope envelope, Guid commitId, bool migrate = true) + { + var eventPayload = envelope.Payload; + + if (migrate && eventPayload is IMigrated migratedEvent) + { + eventPayload = migratedEvent.Migrate(); + + if (ReferenceEquals(migratedEvent, eventPayload)) + { + Debug.WriteLine("Migration should return new event."); + } + } + + var payloadType = typeNameRegistry.GetName(eventPayload.GetType()); + var payloadJson = serializer.Serialize(envelope.Payload); + + envelope.SetCommitId(commitId); + + return new EventData(payloadType, envelope.Headers, payloadJson); + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/Envelope.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Envelope.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/Envelope.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/Envelope.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/EnvelopeHeaders.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeHeaders.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/EnvelopeHeaders.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeHeaders.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs new file mode 100644 index 000000000..4742e8bfb --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.EventSourcing +{ + public class Envelope where T : class, IEvent + { + private readonly EnvelopeHeaders headers; + private readonly T payload; + + public EnvelopeHeaders Headers + { + get { return headers; } + } + + public T Payload + { + get { return payload; } + } + + public Envelope(T payload, EnvelopeHeaders? headers = null) + { + Guard.NotNull(payload); + + this.payload = payload; + + this.headers = headers ?? new EnvelopeHeaders(); + } + + public Envelope To() where TOther : class, IEvent + { + return new Envelope((payload as TOther)!, headers.Clone()); + } + + public static implicit operator Envelope(Envelope source) + { + return source == null ? source! : new Envelope(source.payload, source.headers); + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs new file mode 100644 index 000000000..47198a953 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class EventData + { + public EnvelopeHeaders Headers { get; } + + public string Payload { get; } + + public string Type { get; set; } + + public EventData(string type, EnvelopeHeaders headers, string payload) + { + Guard.NotNull(type); + Guard.NotNull(headers); + Guard.NotNull(payload); + + Headers = headers; + + Payload = payload; + + Type = type; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs new file mode 100644 index 000000000..d997e6a84 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs @@ -0,0 +1,305 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Orleans; +using Orleans.Concurrency; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public class EventConsumerGrain : GrainOfString, IEventConsumerGrain + { + private readonly EventConsumerFactory eventConsumerFactory; + private readonly IGrainState state; + private readonly IEventDataFormatter eventDataFormatter; + private readonly IEventStore eventStore; + private readonly ISemanticLog log; + private TaskScheduler? scheduler; + private IEventSubscription? currentSubscription; + private IEventConsumer? eventConsumer; + + public EventConsumerGrain( + EventConsumerFactory eventConsumerFactory, + IGrainState state, + IEventStore eventStore, + IEventDataFormatter eventDataFormatter, + ISemanticLog log) + { + Guard.NotNull(eventStore); + Guard.NotNull(eventDataFormatter); + Guard.NotNull(eventConsumerFactory); + Guard.NotNull(state); + Guard.NotNull(log); + + this.eventStore = eventStore; + this.eventDataFormatter = eventDataFormatter; + this.eventConsumerFactory = eventConsumerFactory; + this.state = state; + + this.log = log; + } + + protected override Task OnActivateAsync(string key) + { + scheduler = TaskScheduler.Current; + + eventConsumer = eventConsumerFactory(key); + + return TaskHelper.Done; + } + + public Task> GetStateAsync() + { + return Task.FromResult(CreateInfo()); + } + + private Immutable CreateInfo() + { + return state.Value.ToInfo(eventConsumer!.Name).AsImmutable(); + } + + public Task OnEventAsync(Immutable subscription, Immutable storedEvent) + { + if (subscription.Value != currentSubscription) + { + return TaskHelper.Done; + } + + return DoAndUpdateStateAsync(async () => + { + if (eventConsumer!.Handles(storedEvent.Value)) + { + var @event = ParseKnownEvent(storedEvent.Value); + + if (@event != null) + { + await DispatchConsumerAsync(@event); + } + } + + state.Value = state.Value.Handled(storedEvent.Value.EventPosition); + }); + } + + public Task OnErrorAsync(Immutable subscription, Immutable exception) + { + if (subscription.Value != currentSubscription) + { + return TaskHelper.Done; + } + + return DoAndUpdateStateAsync(() => + { + Unsubscribe(); + + state.Value = state.Value.Failed(exception.Value); + }); + } + + public Task ActivateAsync() + { + if (!state.Value.IsStopped) + { + Subscribe(state.Value.Position); + } + + return TaskHelper.Done; + } + + public async Task> StartAsync() + { + if (!state.Value.IsStopped) + { + return CreateInfo(); + } + + await DoAndUpdateStateAsync(() => + { + Subscribe(state.Value.Position); + + state.Value = state.Value.Started(); + }); + + return CreateInfo(); + } + + public async Task> StopAsync() + { + if (state.Value.IsStopped) + { + return CreateInfo(); + } + + await DoAndUpdateStateAsync(() => + { + Unsubscribe(); + + state.Value = state.Value.Stopped(); + }); + + return CreateInfo(); + } + + public async Task> ResetAsync() + { + await DoAndUpdateStateAsync(async () => + { + Unsubscribe(); + + await ClearAsync(); + + Subscribe(null); + + state.Value = state.Value.Reset(); + }); + + return CreateInfo(); + } + + private Task DoAndUpdateStateAsync(Action action, [CallerMemberName] string? caller = null) + { + return DoAndUpdateStateAsync(() => { action(); return TaskHelper.Done; }, caller); + } + + private async Task DoAndUpdateStateAsync(Func action, [CallerMemberName] string? caller = null) + { + try + { + await action(); + } + catch (Exception ex) + { + try + { + Unsubscribe(); + } + catch (Exception unsubscribeException) + { + ex = new AggregateException(ex, unsubscribeException); + } + + log.LogFatal(ex, w => w + .WriteProperty("action", caller) + .WriteProperty("status", "Failed") + .WriteProperty("eventConsumer", eventConsumer!.Name)); + + state.Value = state.Value.Failed(ex); + } + + await state.WriteAsync(); + } + + private async Task ClearAsync() + { + var logContext = (actionId: Guid.NewGuid().ToString(), consumer: eventConsumer.Name); + + log.LogInformation(logContext, (ctx, w) => w + .WriteProperty("action", "EventConsumerReset") + .WriteProperty("actionId", ctx.actionId) + .WriteProperty("status", "Started") + .WriteProperty("eventConsumer", ctx.consumer)); + + using (log.MeasureTrace(logContext, (ctx, w) => w + .WriteProperty("action", "EventConsumerReset") + .WriteProperty("actionId", ctx.actionId) + .WriteProperty("status", "Completed") + .WriteProperty("eventConsumer", ctx.consumer))) + { + await eventConsumer.ClearAsync(); + } + } + + private async Task DispatchConsumerAsync(Envelope @event) + { + var eventId = @event.Headers.EventId().ToString(); + var eventType = @event.Payload.GetType().Name; + + var logContext = (eventId, eventType, consumer: eventConsumer.Name); + + log.LogInformation(logContext, (ctx, w) => w + .WriteProperty("action", "HandleEvent") + .WriteProperty("actionId", ctx.eventId) + .WriteProperty("status", "Started") + .WriteProperty("eventId", ctx.eventId) + .WriteProperty("eventType", ctx.eventType) + .WriteProperty("eventConsumer", ctx.consumer)); + + using (log.MeasureTrace(logContext, (ctx, w) => w + .WriteProperty("action", "HandleEvent") + .WriteProperty("actionId", ctx.eventId) + .WriteProperty("status", "Completed") + .WriteProperty("eventId", ctx.eventId) + .WriteProperty("eventType", ctx.eventType) + .WriteProperty("eventConsumer", ctx.consumer))) + { + await eventConsumer.On(@event); + } + } + + private void Unsubscribe() + { + if (currentSubscription != null) + { + currentSubscription.StopAsync().Forget(); + currentSubscription = null; + } + } + + private void Subscribe(string? position) + { + if (currentSubscription == null) + { + currentSubscription?.StopAsync().Forget(); + currentSubscription = CreateSubscription(eventConsumer!.EventsFilter, position); + } + else + { + currentSubscription.WakeUp(); + } + } + + private Envelope? ParseKnownEvent(StoredEvent message) + { + try + { + var @event = eventDataFormatter.Parse(message.Data); + + @event.SetEventPosition(message.EventPosition); + @event.SetEventStreamNumber(message.EventStreamNumber); + + return @event; + } + catch (TypeNameNotFoundException) + { + log.LogDebug(w => w.WriteProperty("oldEventFound", message.Data.Type)); + + return null; + } + } + + protected virtual IEventConsumerGrain GetSelf() + { + return this.AsReference(); + } + + protected virtual IEventSubscription CreateSubscription(IEventStore store, IEventSubscriber subscriber, string streamFilter, string? position) + { + return new RetrySubscription(store, subscriber, streamFilter, position); + } + + private IEventSubscription CreateSubscription(string streamFilter, string? position) + { + return CreateSubscription(eventStore, new WrapperSubscription(GetSelf(), scheduler!), streamFilter, position); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs new file mode 100644 index 000000000..329dee5da --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Orleans; +using Orleans.Concurrency; +using Orleans.Core; +using Orleans.Runtime; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public class EventConsumerManagerGrain : Grain, IEventConsumerManagerGrain, IRemindable + { + private readonly IEnumerable eventConsumers; + + public EventConsumerManagerGrain(IEnumerable eventConsumers) + : this(eventConsumers, null, null) + { + } + + protected EventConsumerManagerGrain( + IEnumerable eventConsumers, + IGrainIdentity? identity, + IGrainRuntime? runtime) + : base(identity, runtime) + { + Guard.NotNull(eventConsumers); + + this.eventConsumers = eventConsumers; + } + + public override Task OnActivateAsync() + { + DelayDeactivation(TimeSpan.FromDays(1)); + + RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + RegisterTimer(x => ActivateAsync(null), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); + + return Task.FromResult(true); + } + + public Task ActivateAsync(string? streamName) + { + var tasks = + eventConsumers + .Where(c => streamName == null || Regex.IsMatch(streamName, c.EventsFilter)) + .Select(c => GrainFactory.GetGrain(c.Name)) + .Select(c => c.ActivateAsync()); + + return Task.WhenAll(tasks); + } + + public async Task>> GetConsumersAsync() + { + var tasks = + eventConsumers + .Select(c => GrainFactory.GetGrain(c.Name)) + .Select(c => c.GetStateAsync()); + + var consumerInfos = await Task.WhenAll(tasks); + + return new Immutable>(consumerInfos.Select(r => r.Value).ToList()); + } + + public Task StartAllAsync() + { + return Task.WhenAll( + eventConsumers + .Select(c => StartAsync(c.Name))); + } + + public Task StopAllAsync() + { + return Task.WhenAll( + eventConsumers + .Select(c => StopAsync(c.Name))); + } + + public Task> ResetAsync(string consumerName) + { + var eventConsumer = GrainFactory.GetGrain(consumerName); + + return eventConsumer.ResetAsync(); + } + + public Task> StartAsync(string consumerName) + { + var eventConsumer = GrainFactory.GetGrain(consumerName); + + return eventConsumer.StartAsync(); + } + + public Task> StopAsync(string consumerName) + { + var eventConsumer = GrainFactory.GetGrain(consumerName); + + return eventConsumer.StopAsync(); + } + + public Task ActivateAsync() + { + return ActivateAsync(null); + } + + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return ActivateAsync(null); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs new file mode 100644 index 000000000..8a68081fc --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public sealed class EventConsumerState + { + public bool IsStopped { get; set; } + + public string? Error { get; set; } + + public string? Position { get; set; } + + public EventConsumerState Reset() + { + return new EventConsumerState(); + } + + public EventConsumerState Handled(string position) + { + return new EventConsumerState { Position = position }; + } + + public EventConsumerState Failed(Exception ex) + { + return new EventConsumerState { Position = Position, IsStopped = true, Error = ex?.ToString() }; + } + + public EventConsumerState Stopped() + { + return new EventConsumerState { Position = Position, IsStopped = true }; + } + + public EventConsumerState Started() + { + return new EventConsumerState { Position = Position, IsStopped = false }; + } + + public EventConsumerInfo ToInfo(string name) + { + return SimpleMapper.Map(this, new EventConsumerInfo { Name = name }); + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs new file mode 100644 index 000000000..898464730 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Orleans; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public sealed class OrleansEventNotifier : IEventNotifier + { + private readonly Lazy eventConsumerManagerGrain; + + public OrleansEventNotifier(IGrainFactory factory) + { + Guard.NotNull(factory); + + eventConsumerManagerGrain = new Lazy(() => + { + return factory.GetGrain(SingleGrain.Id); + }); + } + + public void NotifyEventsStored(string streamName) + { + eventConsumerManagerGrain.Value.ActivateAsync(streamName); + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/WrapperSubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/WrapperSubscription.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/Grains/WrapperSubscription.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/Grains/WrapperSubscription.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/IEvent.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEvent.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/IEvent.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/IEvent.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventDataFormatter.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventDataFormatter.cs new file mode 100644 index 000000000..38332ea9a --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventDataFormatter.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.EventSourcing +{ + public interface IEventDataFormatter + { + Envelope Parse(EventData eventData, Func? stringConverter = null); + + EventData ToEventData(Envelope envelope, Guid commitId, bool migrate = true); + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventEnricher.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventEnricher.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/IEventEnricher.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/IEventEnricher.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs new file mode 100644 index 000000000..881eb708c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.EventSourcing +{ + public interface IEventStore + { + Task CreateIndexAsync(string property); + + Task> QueryAsync(string streamName, long streamPosition = 0); + + Task QueryAsync(Func callback, string? streamFilter = null, string? position = null, CancellationToken ct = default); + + Task QueryAsync(Func callback, string property, object value, string? position = null, CancellationToken ct = default); + + Task AppendAsync(Guid commitId, string streamName, ICollection events); + + Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events); + + Task DeleteStreamAsync(string streamName); + + IEventSubscription CreateSubscription(IEventSubscriber subscriber, string? streamFilter = null, string? position = null); + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/NoopEvent.cs b/backend/src/Squidex.Infrastructure/EventSourcing/NoopEvent.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/NoopEvent.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/NoopEvent.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs new file mode 100644 index 000000000..9323bdd86 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Timers; + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class PollingSubscription : IEventSubscription + { + private readonly CompletionTimer timer; + + public PollingSubscription( + IEventStore eventStore, + IEventSubscriber eventSubscriber, + string? streamFilter, + string? position) + { + Guard.NotNull(eventStore); + Guard.NotNull(eventSubscriber); + + timer = new CompletionTimer(5000, async ct => + { + try + { + await eventStore.QueryAsync(async storedEvent => + { + await eventSubscriber.OnEventAsync(this, storedEvent); + + position = storedEvent.EventPosition; + }, streamFilter, position, ct); + } + catch (Exception ex) + { + if (!ex.Is()) + { + await eventSubscriber.OnErrorAsync(this, ex); + } + } + }); + } + + public void WakeUp() + { + timer.SkipCurrentDelay(); + } + + public Task StopAsync() + { + return timer.StopAsync(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs new file mode 100644 index 000000000..229e62860 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs @@ -0,0 +1,117 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +#pragma warning disable RECS0002 // Convert anonymous method to method group + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class RetrySubscription : IEventSubscription, IEventSubscriber + { + private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(10); + private readonly CancellationTokenSource timerCts = new CancellationTokenSource(); + private readonly RetryWindow retryWindow = new RetryWindow(TimeSpan.FromMinutes(5), 5); + private readonly IEventStore eventStore; + private readonly IEventSubscriber eventSubscriber; + private readonly string? streamFilter; + private IEventSubscription? currentSubscription; + private string? position; + + public int ReconnectWaitMs { get; set; } = 5000; + + public RetrySubscription(IEventStore eventStore, IEventSubscriber eventSubscriber, string? streamFilter, string? position) + { + Guard.NotNull(eventStore); + Guard.NotNull(eventSubscriber); + Guard.NotNull(streamFilter); + + this.position = position; + + this.eventStore = eventStore; + this.eventSubscriber = eventSubscriber; + + this.streamFilter = streamFilter; + + Subscribe(); + } + + private void Subscribe() + { + if (currentSubscription == null) + { + currentSubscription = eventStore.CreateSubscription(this, streamFilter, position); + } + } + + private void Unsubscribe() + { + currentSubscription?.StopAsync().Forget(); + currentSubscription = null; + } + + public void WakeUp() + { + currentSubscription?.WakeUp(); + } + + private async Task HandleEventAsync(IEventSubscription subscription, StoredEvent storedEvent) + { + if (subscription == currentSubscription) + { + await eventSubscriber.OnEventAsync(this, storedEvent); + + position = storedEvent.EventPosition; + } + } + + private async Task HandleErrorAsync(IEventSubscription subscription, Exception exception) + { + if (subscription == currentSubscription) + { + Unsubscribe(); + + if (retryWindow.CanRetryAfterFailure()) + { + RetryAsync().Forget(); + } + else + { + await eventSubscriber.OnErrorAsync(this, exception); + } + } + } + + private async Task RetryAsync() + { + await Task.Delay(ReconnectWaitMs, timerCts.Token); + + await dispatcher.DispatchAsync(Subscribe); + } + + Task IEventSubscriber.OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) + { + return dispatcher.DispatchAsync(() => HandleEventAsync(subscription, storedEvent)); + } + + Task IEventSubscriber.OnErrorAsync(IEventSubscription subscription, Exception exception) + { + return dispatcher.DispatchAsync(() => HandleErrorAsync(subscription, exception)); + } + + public async Task StopAsync() + { + await dispatcher.DispatchAsync(Unsubscribe); + await dispatcher.StopAndWaitAsync(); + + timerCts.Cancel(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs b/backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs new file mode 100644 index 000000000..e6418d7f3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class StoredEvent + { + public string StreamName { get; } + + public string EventPosition { get; } + + public long EventStreamNumber { get; } + + public EventData Data { get; } + + public StoredEvent(string streamName, string eventPosition, long eventStreamNumber, EventData data) + { + Guard.NotNullOrEmpty(streamName); + Guard.NotNullOrEmpty(eventPosition); + Guard.NotNull(data); + + Data = data; + + EventPosition = eventPosition; + EventStreamNumber = eventStreamNumber; + + StreamName = streamName; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs b/backend/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs new file mode 100644 index 000000000..9a343d050 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/StreamFilter.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.Diagnostics.CodeAnalysis; + +namespace Squidex.Infrastructure.EventSourcing +{ + public static class StreamFilter + { + public static bool IsAll([NotNullWhen(false)] string? filter) + { + return string.IsNullOrWhiteSpace(filter) + || string.Equals(filter, ".*", StringComparison.OrdinalIgnoreCase) + || string.Equals(filter, "(.*)", StringComparison.OrdinalIgnoreCase) + || string.Equals(filter, "(.*?)", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/WrongEventVersionException.cs b/backend/src/Squidex.Infrastructure/EventSourcing/WrongEventVersionException.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/WrongEventVersionException.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/WrongEventVersionException.cs diff --git a/src/Squidex.Infrastructure/ExceptionHelper.cs b/backend/src/Squidex.Infrastructure/ExceptionHelper.cs similarity index 100% rename from src/Squidex.Infrastructure/ExceptionHelper.cs rename to backend/src/Squidex.Infrastructure/ExceptionHelper.cs diff --git a/src/Squidex.Infrastructure/FileExtensions.cs b/backend/src/Squidex.Infrastructure/FileExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/FileExtensions.cs rename to backend/src/Squidex.Infrastructure/FileExtensions.cs diff --git a/src/Squidex.Infrastructure/GravatarHelper.cs b/backend/src/Squidex.Infrastructure/GravatarHelper.cs similarity index 100% rename from src/Squidex.Infrastructure/GravatarHelper.cs rename to backend/src/Squidex.Infrastructure/GravatarHelper.cs diff --git a/backend/src/Squidex.Infrastructure/Guard.cs b/backend/src/Squidex.Infrastructure/Guard.cs new file mode 100644 index 000000000..f98164c84 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Guard.cs @@ -0,0 +1,220 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Infrastructure +{ + public static class Guard + { + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidNumber(float target, [CallerArgumentExpression("target")] string? parameterName = null) + { + if (float.IsNaN(target) || float.IsPositiveInfinity(target) || float.IsNegativeInfinity(target)) + { + throw new ArgumentException("Value must be a valid number.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidNumber(double target, [CallerArgumentExpression("target")] string? parameterName = null) + { + if (double.IsNaN(target) || double.IsPositiveInfinity(target) || double.IsNegativeInfinity(target)) + { + throw new ArgumentException("Value must be a valid number.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidSlug(string? target, [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNullOrEmpty(target, parameterName); + + if (!target!.IsSlug()) + { + throw new ArgumentException("Target is not a valid slug.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidPropertyName(string? target, [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNullOrEmpty(target, parameterName); + + if (!target!.IsPropertyName()) + { + throw new ArgumentException("Target is not a valid property name.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void HasType(object? target, [CallerArgumentExpression("target")] string? parameterName = null) + { + if (target != null && target.GetType() != typeof(T)) + { + throw new ArgumentException($"The parameter must be of type {typeof(T)}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void HasType(object? target, [AllowNull] Type expectedType, [CallerArgumentExpression("target")] string? parameterName = null) + { + if (target != null && expectedType != null && target.GetType() != expectedType) + { + throw new ArgumentException($"The parameter must be of type {expectedType}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Between(TValue target, TValue lower, TValue upper, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (!target.IsBetween(lower, upper)) + { + throw new ArgumentException($"Value must be between {lower} and {upper}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Enum(TEnum target, [CallerArgumentExpression("target")] string? parameterName = null) where TEnum : struct + { + if (!target.IsEnumValue()) + { + throw new ArgumentException($"Value must be a valid enum type {typeof(TEnum)}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void GreaterThan(TValue target, TValue lower, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (target.CompareTo(lower) <= 0) + { + throw new ArgumentException($"Value must be greater than {lower}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void GreaterEquals(TValue target, TValue lower, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (target.CompareTo(lower) < 0) + { + throw new ArgumentException($"Value must be greater or equal to {lower}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LessThan(TValue target, TValue upper, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (target.CompareTo(upper) >= 0) + { + throw new ArgumentException($"Value must be less than {upper}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LessEquals(TValue target, TValue upper, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (target.CompareTo(upper) > 0) + { + throw new ArgumentException($"Value must be less or equal to {upper}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotEmpty(IReadOnlyCollection? target, [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNull(target, parameterName); + + if (target != null && target.Count == 0) + { + throw new ArgumentException("Collection does not contain an item.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotEmpty(Guid target, [CallerArgumentExpression("target")] string? parameterName = null) + { + if (target == Guid.Empty) + { + throw new ArgumentException("Value cannot be empty.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotNull(object? target, [CallerArgumentExpression("target")] string? parameterName = null) + { + if (target == null) + { + throw new ArgumentNullException(parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotDefault(T target, [CallerArgumentExpression("target")] string? parameterName = null) + { + if (Equals(target, default(T)!)) + { + throw new ArgumentException("Value cannot be an the default value.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotNullOrEmpty(string? target, [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNull(target, parameterName); + + if (string.IsNullOrWhiteSpace(target)) + { + throw new ArgumentException("String parameter cannot be null or empty and cannot contain only blanks.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidFileName(string? target, [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNullOrEmpty(target, parameterName); + + if (target.Intersect(Path.GetInvalidFileNameChars()).Any()) + { + throw new ArgumentException("Value contains an invalid character.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Valid(IValidatable? target, [CallerArgumentExpression("target")] string parameterName, Func message) + { + NotNull(target, parameterName); + + target?.Validate(message); + } + } +} diff --git a/src/Squidex.Infrastructure/HashSet.cs b/backend/src/Squidex.Infrastructure/HashSet.cs similarity index 100% rename from src/Squidex.Infrastructure/HashSet.cs rename to backend/src/Squidex.Infrastructure/HashSet.cs diff --git a/backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs b/backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs new file mode 100644 index 000000000..24bcd0c29 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs @@ -0,0 +1,107 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; + +namespace Squidex.Infrastructure.Http +{ + public static class DumpFormatter + { + public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? responseBody) + { + return BuildDump(request, response, null, responseBody, TimeSpan.Zero); + } + + public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? requestBody, string? responseBody) + { + return BuildDump(request, response, requestBody, responseBody, TimeSpan.Zero); + } + + public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? requestBody, string? responseBody, TimeSpan elapsed, bool isTimeout = false) + { + var writer = new StringBuilder(); + + writer.AppendLine("Request:"); + writer.AppendRequest(request, requestBody); + + writer.AppendLine(); + writer.AppendLine(); + + writer.AppendLine("Response:"); + writer.AppendResponse(response, responseBody, elapsed, isTimeout); + + return writer.ToString(); + } + + private static void AppendRequest(this StringBuilder writer, HttpRequestMessage request, string? requestBody) + { + var method = request.Method.ToString().ToUpperInvariant(); + + writer.AppendLine($"{method}: {request.RequestUri} HTTP/{request.Version}"); + + writer.AppendHeaders(request.Headers); + writer.AppendHeaders(request.Content?.Headers); + + if (!string.IsNullOrWhiteSpace(requestBody)) + { + writer.AppendLine(); + writer.AppendLine(requestBody); + } + } + + private static void AppendResponse(this StringBuilder writer, HttpResponseMessage? response, string? responseBody, TimeSpan elapsed, bool isTimeout) + { + if (response != null) + { + var responseCode = (int)response.StatusCode; + var responseText = Enum.GetName(typeof(HttpStatusCode), response.StatusCode); + + writer.AppendLine($"HTTP/{response.Version} {responseCode} {responseText}"); + + writer.AppendHeaders(response.Headers); + writer.AppendHeaders(response.Content?.Headers); + } + + if (!string.IsNullOrWhiteSpace(responseBody)) + { + writer.AppendLine(); + writer.AppendLine(responseBody); + } + + if (response != null && elapsed != TimeSpan.Zero) + { + writer.AppendLine(); + writer.AppendLine($"Elapsed: {elapsed}"); + } + + if (isTimeout) + { + writer.AppendLine($"Timeout after {elapsed}"); + } + } + + private static void AppendHeaders(this StringBuilder writer, HttpHeaders? headers) + { + if (headers == null) + { + return; + } + + foreach (var header in headers) + { + writer.Append(header.Key); + writer.Append(": "); + writer.Append(string.Join("; ", header.Value)); + writer.AppendLine(); + } + } + } +} diff --git a/src/Squidex.Infrastructure/IBackgroundProcess.cs b/backend/src/Squidex.Infrastructure/IBackgroundProcess.cs similarity index 100% rename from src/Squidex.Infrastructure/IBackgroundProcess.cs rename to backend/src/Squidex.Infrastructure/IBackgroundProcess.cs diff --git a/src/Squidex.Infrastructure/IFreezable.cs b/backend/src/Squidex.Infrastructure/IFreezable.cs similarity index 100% rename from src/Squidex.Infrastructure/IFreezable.cs rename to backend/src/Squidex.Infrastructure/IFreezable.cs diff --git a/src/Squidex.Infrastructure/IInitializable.cs b/backend/src/Squidex.Infrastructure/IInitializable.cs similarity index 100% rename from src/Squidex.Infrastructure/IInitializable.cs rename to backend/src/Squidex.Infrastructure/IInitializable.cs diff --git a/src/Squidex.Infrastructure/IResultList.cs b/backend/src/Squidex.Infrastructure/IResultList.cs similarity index 100% rename from src/Squidex.Infrastructure/IResultList.cs rename to backend/src/Squidex.Infrastructure/IResultList.cs diff --git a/src/Squidex.Infrastructure/InstantExtensions.cs b/backend/src/Squidex.Infrastructure/InstantExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/InstantExtensions.cs rename to backend/src/Squidex.Infrastructure/InstantExtensions.cs diff --git a/backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs b/backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs new file mode 100644 index 000000000..c544520ed --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/IJsonSerializer.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.IO; + +namespace Squidex.Infrastructure.Json +{ + public interface IJsonSerializer + { + string Serialize(T value, bool intented = false); + + void Serialize(T value, Stream stream); + + T Deserialize(string value, Type? actualType = null, Func? stringConverter = null); + + T Deserialize(Stream stream, Type? actualType = null, Func? stringConverter = null); + } +} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/ClaimsPrincipalConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/ClaimsPrincipalConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/ClaimsPrincipalConverter.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/ClaimsPrincipalConverter.cs diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs new file mode 100644 index 000000000..dced0314d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs @@ -0,0 +1,100 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Squidex.Infrastructure.Json.Newtonsoft +{ + public sealed class ConverterContractResolver : CamelCasePropertyNamesContractResolver + { + private readonly JsonConverter[] converters; + private readonly object lockObject = new object(); + private Dictionary converterCache = new Dictionary(); + + public ConverterContractResolver(params JsonConverter[] converters) + { + NamingStrategy = new CamelCaseNamingStrategy(false, true); + + this.converters = converters; + + foreach (var converter in converters) + { + if (converter is ISupportedTypes supportedTypes) + { + foreach (var type in supportedTypes.SupportedTypes) + { + converterCache[type] = converter; + } + } + } + } + + protected override JsonArrayContract CreateArrayContract(Type objectType) + { + if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IReadOnlyList<>)) + { + var implementationType = typeof(List<>).MakeGenericType(objectType.GetGenericArguments()); + + return base.CreateArrayContract(implementationType); + } + + return base.CreateArrayContract(objectType); + } + + protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) + { + if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)) + { + var implementationType = typeof(Dictionary<,>).MakeGenericType(objectType.GetGenericArguments()); + + return base.CreateDictionaryContract(implementationType); + } + + return base.CreateDictionaryContract(objectType); + } + + protected override JsonConverter? ResolveContractConverter(Type objectType) + { + JsonConverter? result = base.ResolveContractConverter(objectType); + + if (result != null) + { + return result; + } + + var cache = converterCache; + + if (cache == null || !cache.TryGetValue(objectType, out result)) + { + foreach (var converter in converters) + { + if (converter.CanConvert(objectType)) + { + result = converter; + } + } + + lock (lockObject) + { + cache = converterCache; + + var updatedCache = (cache != null) + ? new Dictionary(cache) + : new Dictionary(); + updatedCache[objectType] = result; + + converterCache = updatedCache; + } + } + + return result; + } + } +} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/EnvelopeHeadersConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/EnvelopeHeadersConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/EnvelopeHeadersConverter.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/EnvelopeHeadersConverter.cs diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/ISupportedTypes.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/ISupportedTypes.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/ISupportedTypes.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/ISupportedTypes.cs diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/InstantConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/InstantConverter.cs new file mode 100644 index 000000000..8b405f652 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/InstantConverter.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using NodaTime; +using NodaTime.Text; + +namespace Squidex.Infrastructure.Json.Newtonsoft +{ + public sealed class InstantConverter : JsonConverter + { + public IEnumerable SupportedTypes + { + get + { + yield return typeof(Instant); + yield return typeof(Instant?); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + { + writer.WriteValue(value.ToString()); + } + else + { + writer.WriteNull(); + } + } + + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String) + { + return InstantPattern.General.Parse(reader.Value.ToString()).Value; + } + + if (reader.TokenType == JsonToken.Date) + { + return Instant.FromDateTimeUtc((DateTime)reader.Value); + } + + if (reader.TokenType == JsonToken.Null && objectType == typeof(Instant?)) + { + return null; + } + + throw new JsonException($"Not a valid date time, expected String or Date, but got {reader.TokenType}."); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Instant) || objectType == typeof(Instant?); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs new file mode 100644 index 000000000..1709db763 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Squidex.Infrastructure.Json.Newtonsoft +{ + public abstract class JsonClassConverter : JsonConverter, ISupportedTypes where T : class + { + public virtual IEnumerable SupportedTypes + { + get { yield return typeof(T); } + } + + public sealed override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + return ReadValue(reader, objectType, serializer); + } + + protected abstract T ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer); + + public sealed override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + WriteValue(writer, (T)value, serializer); + } + + protected abstract void WriteValue(JsonWriter writer, T value, JsonSerializer serializer); + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(T); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs new file mode 100644 index 000000000..88231ce51 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs @@ -0,0 +1,184 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Globalization; +using Newtonsoft.Json; +using Squidex.Infrastructure.Json.Objects; + +#pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator + +namespace Squidex.Infrastructure.Json.Newtonsoft +{ + public class JsonValueConverter : JsonConverter, ISupportedTypes + { + private readonly HashSet supportedTypes = new HashSet + { + typeof(IJsonValue), + typeof(JsonArray), + typeof(JsonBoolean), + typeof(JsonNull), + typeof(JsonNumber), + typeof(JsonObject), + typeof(JsonString) + }; + + public virtual IEnumerable SupportedTypes + { + get { return supportedTypes; } + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return ReadJson(reader); + } + + private static IJsonValue ReadJson(JsonReader reader) + { + switch (reader.TokenType) + { + case JsonToken.Comment: + reader.Read(); + break; + case JsonToken.StartObject: + { + var result = JsonValue.Object(); + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + var propertyName = reader.Value.ToString()!; + + if (!reader.Read()) + { + throw new JsonSerializationException("Unexpected end when reading Object."); + } + + var value = ReadJson(reader); + + result[propertyName] = value; + break; + case JsonToken.EndObject: + return result; + } + } + + throw new JsonSerializationException("Unexpected end when reading Object."); + } + + case JsonToken.StartArray: + { + var result = JsonValue.Array(); + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonToken.Comment: + break; + default: + var value = ReadJson(reader); + + result.Add(value); + break; + case JsonToken.EndArray: + return result; + } + } + + throw new JsonSerializationException("Unexpected end when reading Object."); + } + + case JsonToken.Integer: + return JsonValue.Create((long)reader.Value); + case JsonToken.Float: + return JsonValue.Create((double)reader.Value); + case JsonToken.Boolean: + return JsonValue.Create((bool)reader.Value); + case JsonToken.Date: + return JsonValue.Create(((DateTime)reader.Value).ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); + case JsonToken.String: + return JsonValue.Create(reader.Value.ToString()); + case JsonToken.Null: + case JsonToken.Undefined: + return JsonValue.Null; + } + + throw new NotSupportedException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + WriteJson(writer, (IJsonValue)value); + } + + private static void WriteJson(JsonWriter writer, IJsonValue value) + { + switch (value) + { + case JsonNull _: + writer.WriteNull(); + break; + case JsonBoolean s: + writer.WriteValue(s.Value); + break; + case JsonString s: + writer.WriteValue(s.Value); + break; + case JsonNumber s: + + if (s.Value % 1 == 0) + { + writer.WriteValue((long)s.Value); + } + else + { + writer.WriteValue(s.Value); + } + + break; + case JsonArray array: + writer.WriteStartArray(); + + for (var i = 0; i < array.Count; i++) + { + WriteJson(writer, array[i]); + } + + writer.WriteEndArray(); + break; + + case JsonObject obj: + writer.WriteStartObject(); + + foreach (var kvp in obj) + { + writer.WritePropertyName(kvp.Key); + + WriteJson(writer, kvp.Value); + } + + writer.WriteEndObject(); + break; + } + } + + public override bool CanConvert(Type objectType) + { + return supportedTypes.Contains(objectType); + } + } +} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs new file mode 100644 index 000000000..7a02a55a3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs @@ -0,0 +1,100 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using Newtonsoft.Json; + +namespace Squidex.Infrastructure.Json.Newtonsoft +{ + public sealed class NewtonsoftJsonSerializer : IJsonSerializer + { + private readonly JsonSerializerSettings settings; + private readonly JsonSerializer serializer; + + private sealed class CustomReader : JsonTextReader + { + private readonly Func stringConverter; + + public override object Value + { + get + { + var value = base.Value; + + if (value is string s) + { + return stringConverter(s); + } + + return value; + } + } + + public CustomReader(TextReader reader, Func stringConverter) + : base(reader) + { + this.stringConverter = stringConverter; + } + } + + public NewtonsoftJsonSerializer(JsonSerializerSettings settings) + { + Guard.NotNull(settings); + + this.settings = settings; + + serializer = JsonSerializer.Create(settings); + } + + public string Serialize(T value, bool intented) + { + return JsonConvert.SerializeObject(value, intented ? Formatting.Indented : Formatting.None, settings); + } + + public void Serialize(T value, Stream stream) + { + using (var writer = new StreamWriter(stream)) + { + serializer.Serialize(writer, value); + + writer.Flush(); + } + } + + public T Deserialize(string value, Type? actualType = null, Func? stringConverter = null) + { + using (var textReader = new StringReader(value)) + { + actualType ??= typeof(T); + + using (var reader = GetReader(stringConverter, textReader)) + { + return (T)serializer.Deserialize(reader, actualType); + } + } + } + + public T Deserialize(Stream stream, Type? actualType = null, Func? stringConverter = null) + { + using (var textReader = new StreamReader(stream)) + { + actualType ??= typeof(T); + + using (var reader = GetReader(stringConverter, textReader)) + { + return (T)serializer.Deserialize(reader, actualType); + } + } + } + + private static JsonTextReader GetReader(Func? stringConverter, TextReader textReader) + { + return stringConverter != null ? new CustomReader(textReader, stringConverter) : new JsonTextReader(textReader); + } + } +} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs new file mode 100644 index 000000000..d3dce95b6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Newtonsoft.Json.Serialization; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Infrastructure.Json.Newtonsoft +{ + public sealed class TypeNameSerializationBinder : DefaultSerializationBinder + { + private readonly TypeNameRegistry typeNameRegistry; + + public TypeNameSerializationBinder(TypeNameRegistry typeNameRegistry) + { + Guard.NotNull(typeNameRegistry); + + this.typeNameRegistry = typeNameRegistry; + } + + public override Type BindToType(string assemblyName, string typeName) + { + var type = typeNameRegistry.GetTypeOrNull(typeName); + + return type ?? base.BindToType(assemblyName, typeName); + } + + public override void BindToName(Type serializedType, out string? assemblyName, out string typeName) + { + assemblyName = null; + + var name = typeNameRegistry.GetNameOrNull(serializedType); + + if (name != null) + { + typeName = name; + } + else + { + base.BindToName(serializedType, out assemblyName, out typeName); + } + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs b/backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs new file mode 100644 index 000000000..34596e2d2 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Json.Objects +{ + public interface IJsonValue : IEquatable + { + JsonValueType Type { get; } + + string ToJsonString(); + + string ToString(); + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs new file mode 100644 index 000000000..efbfffdeb --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs @@ -0,0 +1,96 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Squidex.Infrastructure.Json.Objects +{ + public sealed class JsonArray : Collection, IJsonValue, IEquatable + { + public JsonValueType Type + { + get { return JsonValueType.Array; } + } + + public JsonArray() + { + } + + internal JsonArray(params object?[] values) + : base(ToList(values)) + { + } + + private static List ToList(IEnumerable values) + { + return values?.Select(JsonValue.Create).ToList() ?? new List(); + } + + protected override void InsertItem(int index, IJsonValue item) + { + base.InsertItem(index, item ?? JsonValue.Null); + } + + protected override void SetItem(int index, IJsonValue item) + { + base.SetItem(index, item ?? JsonValue.Null); + } + + public override bool Equals(object? obj) + { + return Equals(obj as JsonArray); + } + + public bool Equals(IJsonValue? other) + { + return Equals(other as JsonArray); + } + + public bool Equals(JsonArray? array) + { + if (array == null || array.Count != Count) + { + return false; + } + + for (var i = 0; i < Count; i++) + { + if (!this[i].Equals(array[i])) + { + return false; + } + } + + return true; + } + + public override int GetHashCode() + { + var hashCode = 17; + + for (var i = 0; i < Count; i++) + { + hashCode = (hashCode * 23) + this[i].GetHashCode(); + } + + return hashCode; + } + + public string ToJsonString() + { + return ToString(); + } + + public override string ToString() + { + return $"[{string.Join(", ", this.Select(x => x.ToJsonString()))}]"; + } + } +} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonBoolean.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonBoolean.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Objects/JsonBoolean.cs rename to backend/src/Squidex.Infrastructure/Json/Objects/JsonBoolean.cs diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs new file mode 100644 index 000000000..8d8cfe754 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Json.Objects +{ + public sealed class JsonNull : IJsonValue, IEquatable + { + public static readonly JsonNull Null = new JsonNull(); + + public JsonValueType Type + { + get { return JsonValueType.Null; } + } + + private JsonNull() + { + } + + public override bool Equals(object? obj) + { + return Equals(obj as JsonNull); + } + + public bool Equals(IJsonValue? other) + { + return Equals(other as JsonNull); + } + + public bool Equals(JsonNull? other) + { + return other != null; + } + + public override int GetHashCode() + { + return 0; + } + + public string ToJsonString() + { + return ToString(); + } + + public override string ToString() + { + return "null"; + } + } +} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonNumber.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonNumber.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Objects/JsonNumber.cs rename to backend/src/Squidex.Infrastructure/Json/Objects/JsonNumber.cs diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs new file mode 100644 index 000000000..29dfa2b05 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs @@ -0,0 +1,136 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Squidex.Infrastructure.Json.Objects +{ + public class JsonObject : IReadOnlyDictionary, IJsonValue, IEquatable + { + private readonly Dictionary inner; + + public IJsonValue this[string key] + { + get + { + return inner[key]; + } + set + { + Guard.NotNull(key); + + inner[key] = value ?? JsonValue.Null; + } + } + + public IEnumerable Keys + { + get { return inner.Keys; } + } + + public IEnumerable Values + { + get { return inner.Values; } + } + + public int Count + { + get { return inner.Count; } + } + + public JsonValueType Type + { + get { return JsonValueType.Array; } + } + + internal JsonObject() + { + inner = new Dictionary(); + } + + public JsonObject(JsonObject obj) + { + inner = new Dictionary(obj.inner); + } + + public JsonObject Add(string key, object? value) + { + return Add(key, JsonValue.Create(value)); + } + + public JsonObject Add(string key, IJsonValue? value) + { + inner[key] = value ?? JsonValue.Null; + + return this; + } + + public void Clear() + { + inner.Clear(); + } + + public bool Remove(string key) + { + return inner.Remove(key); + } + + public bool ContainsKey(string key) + { + return inner.ContainsKey(key); + } + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out IJsonValue value) + { + return inner.TryGetValue(key, out value!); + } + + public IEnumerator> GetEnumerator() + { + return inner.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return inner.GetEnumerator(); + } + + public override bool Equals(object? obj) + { + return Equals(obj as JsonObject); + } + + public bool Equals(IJsonValue other) + { + return Equals(other as JsonObject); + } + + public bool Equals(JsonObject? other) + { + return other != null && inner.EqualsDictionary(other.inner); + } + + public override int GetHashCode() + { + return inner.DictionaryHashCode(); + } + + public string ToJsonString() + { + return ToString(); + } + + public override string ToString() + { + return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value.ToJsonString()}"))}}}"; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs new file mode 100644 index 000000000..8b0ab1c12 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Json.Objects +{ + public abstract class JsonScalar : IJsonValue, IEquatable> where T : notnull + { + public abstract JsonValueType Type { get; } + + public T Value { get; } + + protected JsonScalar(T value) + { + Value = value; + } + + public override bool Equals(object? obj) + { + return Equals(obj as JsonScalar); + } + + public bool Equals(IJsonValue? other) + { + return Equals(other as JsonScalar); + } + + public bool Equals(JsonScalar? other) + { + return other != null && other.Type == Type && Equals(other.Value, Value); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public override string ToString() + { + return Value.ToString()!; + } + + public virtual string ToJsonString() + { + return ToString(); + } + } +} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonString.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonString.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Objects/JsonString.cs rename to backend/src/Squidex.Infrastructure/Json/Objects/JsonString.cs diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs new file mode 100644 index 000000000..dfe024aad --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs @@ -0,0 +1,136 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using NodaTime; + +#pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator + +namespace Squidex.Infrastructure.Json.Objects +{ + public static class JsonValue + { + public static readonly IJsonValue Empty = new JsonString(string.Empty); + + public static readonly IJsonValue True = JsonBoolean.True; + public static readonly IJsonValue False = JsonBoolean.False; + + public static readonly IJsonValue Null = JsonNull.Null; + + public static readonly IJsonValue Zero = new JsonNumber(0); + + public static JsonArray Array() + { + return new JsonArray(); + } + + public static JsonArray Array(params object?[] values) + { + return new JsonArray(values); + } + + public static JsonObject Object() + { + return new JsonObject(); + } + + public static IJsonValue Create(object? value) + { + if (value == null) + { + return Null; + } + + if (value is IJsonValue v) + { + return v; + } + + switch (value) + { + case string s: + return Create(s); + case bool b: + return Create(b); + case float f: + return Create(f); + case double d: + return Create(d); + case int i: + return Create(i); + case long l: + return Create(l); + case Instant i: + return Create(i); + } + + throw new ArgumentException("Invalid json type"); + } + + public static IJsonValue Create(bool value) + { + return value ? True : False; + } + + public static IJsonValue Create(double value) + { + Guard.ValidNumber(value); + + if (value == 0) + { + return Zero; + } + + return new JsonNumber(value); + } + + public static IJsonValue Create(Instant? value) + { + if (value == null) + { + return Null; + } + + return Create(value.Value.ToString()); + } + + public static IJsonValue Create(double? value) + { + if (value == null) + { + return Null; + } + + return Create(value.Value); + } + + public static IJsonValue Create(bool? value) + { + if (value == null) + { + return Null; + } + + return Create(value.Value); + } + + public static IJsonValue Create(string? value) + { + if (value == null) + { + return Null; + } + + if (value.Length == 0) + { + return Empty; + } + + return new JsonString(value); + } + } +} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonValueType.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValueType.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Objects/JsonValueType.cs rename to backend/src/Squidex.Infrastructure/Json/Objects/JsonValueType.cs diff --git a/backend/src/Squidex.Infrastructure/Language.cs b/backend/src/Squidex.Infrastructure/Language.cs new file mode 100644 index 000000000..9a86fb6ba --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Language.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; + +namespace Squidex.Infrastructure +{ + public sealed partial class Language + { + private static readonly Regex CultureRegex = new Regex("^([a-z]{2})(\\-[a-z]{2})?$", RegexOptions.IgnoreCase); + private static readonly Dictionary AllLanguagesField = new Dictionary(StringComparer.OrdinalIgnoreCase); + + internal static Language AddLanguage(string iso2Code, string englishName) + { + return AllLanguagesField.GetOrAdd(iso2Code, englishName, (c, n) => new Language(c, n)); + } + + public static Language GetLanguage(string iso2Code) + { + Guard.NotNullOrEmpty(iso2Code); + + try + { + return AllLanguagesField[iso2Code]; + } + catch (KeyNotFoundException) + { + throw new NotSupportedException($"Language {iso2Code} is not supported"); + } + } + + public static IReadOnlyCollection AllLanguages + { + get { return AllLanguagesField.Values; } + } + + public string EnglishName { get; } + + public string Iso2Code { get; } + + private Language(string iso2Code, string englishName) + { + Iso2Code = iso2Code; + + EnglishName = englishName; + } + + public static bool IsValidLanguage(string iso2Code) + { + Guard.NotNullOrEmpty(iso2Code); + + return AllLanguagesField.ContainsKey(iso2Code); + } + + public static bool TryGetLanguage(string iso2Code, [MaybeNullWhen(false)] out Language language) + { + Guard.NotNullOrEmpty(iso2Code); + + return AllLanguagesField.TryGetValue(iso2Code, out language!); + } + + public static implicit operator string(Language language) + { + return language.Iso2Code; + } + + public static implicit operator Language(string iso2Code) + { + return GetLanguage(iso2Code!); + } + + public static Language? ParseOrNull(string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + return null; + } + + input = input.Trim(); + + if (input.Length != 2) + { + var match = CultureRegex.Match(input); + + if (!match.Success) + { + return null; + } + + input = match.Groups[1].Value; + } + + if (TryGetLanguage(input.ToLowerInvariant(), out var result)) + { + return result; + } + + return null; + } + + public override string ToString() + { + return EnglishName; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Languages.cs b/backend/src/Squidex.Infrastructure/Languages.cs similarity index 100% rename from src/Squidex.Infrastructure/Languages.cs rename to backend/src/Squidex.Infrastructure/Languages.cs diff --git a/backend/src/Squidex.Infrastructure/LanguagesInitializer.cs b/backend/src/Squidex.Infrastructure/LanguagesInitializer.cs new file mode 100644 index 000000000..d2ce13965 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/LanguagesInitializer.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure +{ + public sealed class LanguagesInitializer : IInitializable + { + private readonly LanguagesOptions options; + + public LanguagesInitializer(IOptions options) + { + Guard.NotNull(options); + + this.options = options.Value; + } + + public Task InitializeAsync(CancellationToken ct = default) + { + foreach (var kvp in options) + { + if (!string.IsNullOrWhiteSpace(kvp.Key) && !string.IsNullOrWhiteSpace(kvp.Value)) + { + Language.AddLanguage(kvp.Key, kvp.Value); + } + } + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Infrastructure/LanguagesOptions.cs b/backend/src/Squidex.Infrastructure/LanguagesOptions.cs similarity index 100% rename from src/Squidex.Infrastructure/LanguagesOptions.cs rename to backend/src/Squidex.Infrastructure/LanguagesOptions.cs diff --git a/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLogger.cs b/backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLogger.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/Adapter/SemanticLogLogger.cs rename to backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLogger.cs diff --git a/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs b/backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs rename to backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs diff --git a/backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs b/backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs new file mode 100644 index 000000000..931f3312f --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Squidex.Infrastructure.Log.Adapter +{ + public class SemanticLogLoggerProvider : ILoggerProvider + { + private readonly IServiceProvider services; + private ISemanticLog? log; + + public SemanticLogLoggerProvider(IServiceProvider services) + { + Guard.NotNull(services); + + this.services = services; + } + + internal SemanticLogLoggerProvider(ISemanticLog? log) + { + this.log = log; + } + + public static SemanticLogLoggerProvider ForTesting(ISemanticLog? log) + { + return new SemanticLogLoggerProvider(log); + } + + public ILogger CreateLogger(string categoryName) + { + if (log == null && services != null) + { + log = services.GetService(typeof(ISemanticLog)) as ISemanticLog; + } + + if (log == null) + { + return NullLogger.Instance; + } + + return new SemanticLogLogger(log.CreateScope(writer => + { + writer.WriteProperty("category", categoryName); + })); + } + + public void Dispose() + { + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs b/backend/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs new file mode 100644 index 000000000..55af0fa92 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Reflection; + +namespace Squidex.Infrastructure.Log +{ + public sealed class ApplicationInfoLogAppender : ILogAppender + { + private readonly string applicationName; + private readonly string applicationVersion; + private readonly string applicationSessionId; + + public ApplicationInfoLogAppender(Type type, Guid applicationSession) + : this(type?.Assembly!, applicationSession) + { + } + + public ApplicationInfoLogAppender(Assembly assembly, Guid applicationSession) + { + Guard.NotNull(assembly); + + applicationName = assembly.GetName().Name!; + applicationVersion = assembly.GetName().Version!.ToString()!; + applicationSessionId = applicationSession.ToString(); + } + + public void Append(IObjectWriter writer, SemanticLogLevel logLevel) + { + writer.WriteObject("app", w => w + .WriteProperty("name", applicationName) + .WriteProperty("version", applicationVersion) + .WriteProperty("sessionId", applicationSessionId)); + } + } +} diff --git a/src/Squidex.Infrastructure/Log/ConsoleLogChannel.cs b/backend/src/Squidex.Infrastructure/Log/ConsoleLogChannel.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/ConsoleLogChannel.cs rename to backend/src/Squidex.Infrastructure/Log/ConsoleLogChannel.cs diff --git a/backend/src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs b/backend/src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs new file mode 100644 index 000000000..4fcb839ab --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Log +{ + public sealed class ConstantsLogWriter : ILogAppender + { + private readonly Action objectWriter; + + public ConstantsLogWriter(Action objectWriter) + { + Guard.NotNull(objectWriter); + + this.objectWriter = objectWriter; + } + + public void Append(IObjectWriter writer, SemanticLogLevel logLevel) + { + objectWriter(writer); + } + } +} diff --git a/src/Squidex.Infrastructure/Log/DebugLogChannel.cs b/backend/src/Squidex.Infrastructure/Log/DebugLogChannel.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/DebugLogChannel.cs rename to backend/src/Squidex.Infrastructure/Log/DebugLogChannel.cs diff --git a/backend/src/Squidex.Infrastructure/Log/FileChannel.cs b/backend/src/Squidex.Infrastructure/Log/FileChannel.cs new file mode 100644 index 000000000..897aa0693 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/FileChannel.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Log.Internal; + +namespace Squidex.Infrastructure.Log +{ + public sealed class FileChannel : DisposableObjectBase, ILogChannel + { + private readonly FileLogProcessor processor; + private readonly object lockObject = new object(); + private volatile bool isInitialized; + + public FileChannel(string path) + { + Guard.NotNullOrEmpty(path); + + processor = new FileLogProcessor(path); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + processor.Dispose(); + } + } + + public void Log(SemanticLogLevel logLevel, string message) + { + if (!isInitialized) + { + lock (lockObject) + { + if (!isInitialized) + { + processor.Initialize(); + + isInitialized = true; + } + } + } + + processor.EnqueueMessage(new LogMessageEntry { Message = message }); + } + } +} diff --git a/src/Squidex.Infrastructure/Log/IArrayWriter.cs b/backend/src/Squidex.Infrastructure/Log/IArrayWriter.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/IArrayWriter.cs rename to backend/src/Squidex.Infrastructure/Log/IArrayWriter.cs diff --git a/src/Squidex.Infrastructure/Log/ILogAppender.cs b/backend/src/Squidex.Infrastructure/Log/ILogAppender.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/ILogAppender.cs rename to backend/src/Squidex.Infrastructure/Log/ILogAppender.cs diff --git a/src/Squidex.Infrastructure/Log/ILogChannel.cs b/backend/src/Squidex.Infrastructure/Log/ILogChannel.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/ILogChannel.cs rename to backend/src/Squidex.Infrastructure/Log/ILogChannel.cs diff --git a/src/Squidex.Infrastructure/Log/ILogStore.cs b/backend/src/Squidex.Infrastructure/Log/ILogStore.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/ILogStore.cs rename to backend/src/Squidex.Infrastructure/Log/ILogStore.cs diff --git a/backend/src/Squidex.Infrastructure/Log/IObjectWriter.cs b/backend/src/Squidex.Infrastructure/Log/IObjectWriter.cs new file mode 100644 index 000000000..5f4e2b5cb --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/IObjectWriter.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using NodaTime; + +namespace Squidex.Infrastructure.Log +{ + public interface IObjectWriter + { + IObjectWriter WriteProperty(string property, string? value); + + IObjectWriter WriteProperty(string property, double value); + + IObjectWriter WriteProperty(string property, long value); + + IObjectWriter WriteProperty(string property, bool value); + + IObjectWriter WriteProperty(string property, TimeSpan value); + + IObjectWriter WriteProperty(string property, Instant value); + + IObjectWriter WriteObject(string property, Action objectWriter); + + IObjectWriter WriteObject(string property, T context, Action objectWriter); + + IObjectWriter WriteArray(string property, Action arrayWriter); + + IObjectWriter WriteArray(string property, T context, Action arrayWriter); + + string ToString(); + } +} diff --git a/src/Squidex.Infrastructure/Log/IObjectWriterFactory.cs b/backend/src/Squidex.Infrastructure/Log/IObjectWriterFactory.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/IObjectWriterFactory.cs rename to backend/src/Squidex.Infrastructure/Log/IObjectWriterFactory.cs diff --git a/src/Squidex.Infrastructure/Log/ISemanticLog.cs b/backend/src/Squidex.Infrastructure/Log/ISemanticLog.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/ISemanticLog.cs rename to backend/src/Squidex.Infrastructure/Log/ISemanticLog.cs diff --git a/src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs b/backend/src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs rename to backend/src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs diff --git a/backend/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs b/backend/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs new file mode 100644 index 000000000..a18c4af1a --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Squidex.Infrastructure.Log.Internal +{ + public sealed class ConsoleLogProcessor : DisposableObjectBase + { + private const int MaxQueuedMessages = 1024; + private readonly IConsole console; + private readonly BlockingCollection messageQueue = new BlockingCollection(MaxQueuedMessages); + private readonly Thread outputThread; + + public ConsoleLogProcessor() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + console = new WindowsLogConsole(true); + } + else + { + console = new AnsiLogConsole(false); + } + + outputThread = new Thread(ProcessLogQueue) + { + IsBackground = true, Name = "Logging" + }; + + outputThread.Start(); + } + + public void EnqueueMessage(LogMessageEntry message) + { + if (!messageQueue.IsAddingCompleted) + { + try + { + messageQueue.Add(message); + return; + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to enqueue log message: {ex}."); + } + } + + WriteMessage(message); + } + + private void ProcessLogQueue() + { + try + { + foreach (var message in messageQueue.GetConsumingEnumerable()) + { + WriteMessage(message); + } + } + catch + { + try + { + messageQueue.CompleteAdding(); + } + catch + { + return; + } + } + } + + private void WriteMessage(LogMessageEntry entry) + { + console.WriteLine(entry.Color, entry.Message); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + messageQueue.CompleteAdding(); + + try + { + outputThread.Join(1500); + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to shutdown log queue grateful: {ex}."); + } + finally + { + console.Reset(); + } + } + } + } +} diff --git a/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs b/backend/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs rename to backend/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs diff --git a/src/Squidex.Infrastructure/Log/Internal/IConsole.cs b/backend/src/Squidex.Infrastructure/Log/Internal/IConsole.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/Internal/IConsole.cs rename to backend/src/Squidex.Infrastructure/Log/Internal/IConsole.cs diff --git a/src/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs b/backend/src/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs rename to backend/src/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs diff --git a/src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs b/backend/src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs rename to backend/src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs diff --git a/backend/src/Squidex.Infrastructure/Log/JsonLogWriter.cs b/backend/src/Squidex.Infrastructure/Log/JsonLogWriter.cs new file mode 100644 index 000000000..aa28689d8 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/JsonLogWriter.cs @@ -0,0 +1,225 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Text; +using System.Text.Json; +using NodaTime; + +namespace Squidex.Infrastructure.Log +{ + public sealed class JsonLogWriter : IObjectWriter, IArrayWriter + { + private readonly JsonWriterOptions formatting; + private readonly bool formatLine; + private readonly MemoryStream stream = new MemoryStream(); + private readonly StreamReader streamReader; + private Utf8JsonWriter jsonWriter; + + public long BufferSize + { + get { return stream.Length; } + } + + internal JsonLogWriter(JsonWriterOptions formatting, bool formatLine) + { + this.formatLine = formatLine; + this.formatting = formatting; + + streamReader = new StreamReader(stream, Encoding.UTF8); + + Start(); + } + + private void Start() + { + jsonWriter = new Utf8JsonWriter(stream, formatting); + jsonWriter.WriteStartObject(); + } + + internal void Reset() + { + stream.Position = 0; + stream.SetLength(0); + + Start(); + } + + IArrayWriter IArrayWriter.WriteValue(string value) + { + jsonWriter.WriteStringValue(value); + + return this; + } + + IArrayWriter IArrayWriter.WriteValue(double value) + { + jsonWriter.WriteNumberValue(value); + + return this; + } + + IArrayWriter IArrayWriter.WriteValue(long value) + { + jsonWriter.WriteNumberValue(value); + + return this; + } + + IArrayWriter IArrayWriter.WriteValue(bool value) + { + jsonWriter.WriteBooleanValue(value); + + return this; + } + + IArrayWriter IArrayWriter.WriteValue(Instant value) + { + jsonWriter.WriteStringValue(value.ToString()); + + return this; + } + + IArrayWriter IArrayWriter.WriteValue(TimeSpan value) + { + jsonWriter.WriteStringValue(value.ToString()); + + return this; + } + + IObjectWriter IObjectWriter.WriteProperty(string property, string? value) + { + jsonWriter.WriteString(property, value); + + return this; + } + + IObjectWriter IObjectWriter.WriteProperty(string property, double value) + { + jsonWriter.WriteNumber(property, value); + + return this; + } + + IObjectWriter IObjectWriter.WriteProperty(string property, long value) + { + jsonWriter.WriteNumber(property, value); + + return this; + } + + IObjectWriter IObjectWriter.WriteProperty(string property, bool value) + { + jsonWriter.WriteBoolean(property, value); + + return this; + } + + IObjectWriter IObjectWriter.WriteProperty(string property, Instant value) + { + jsonWriter.WriteString(property, value.ToString()); + + return this; + } + + IObjectWriter IObjectWriter.WriteProperty(string property, TimeSpan value) + { + jsonWriter.WriteString(property, value.ToString()); + + return this; + } + + IObjectWriter IObjectWriter.WriteObject(string property, Action objectWriter) + { + jsonWriter.WritePropertyName(property); + jsonWriter.WriteStartObject(); + + objectWriter?.Invoke(this); + + jsonWriter.WriteEndObject(); + + return this; + } + + IObjectWriter IObjectWriter.WriteObject(string property, T context, Action objectWriter) + { + jsonWriter.WritePropertyName(property); + jsonWriter.WriteStartObject(); + + objectWriter?.Invoke(context, this); + + jsonWriter.WriteEndObject(); + + return this; + } + + IObjectWriter IObjectWriter.WriteArray(string property, Action arrayWriter) + { + jsonWriter.WritePropertyName(property); + jsonWriter.WriteStartArray(); + + arrayWriter?.Invoke(this); + + jsonWriter.WriteEndArray(); + + return this; + } + + IObjectWriter IObjectWriter.WriteArray(string property, T context, Action arrayWriter) + { + jsonWriter.WritePropertyName(property); + jsonWriter.WriteStartArray(); + + arrayWriter?.Invoke(context, this); + + jsonWriter.WriteEndArray(); + + return this; + } + + IArrayWriter IArrayWriter.WriteObject(Action objectWriter) + { + jsonWriter.WriteStartObject(); + + objectWriter?.Invoke(this); + + jsonWriter.WriteEndObject(); + + return this; + } + + IArrayWriter IArrayWriter.WriteObject(T context, Action objectWriter) + { + jsonWriter.WriteStartObject(); + + objectWriter?.Invoke(context, this); + + jsonWriter.WriteEndObject(); + + return this; + } + + public override string ToString() + { + jsonWriter.WriteEndObject(); + jsonWriter.Flush(); + + stream.Position = 0; + streamReader.DiscardBufferedData(); + + var json = streamReader.ReadToEnd(); + + if (formatLine) + { + json += Environment.NewLine; + } + + return json; + } + } +} diff --git a/src/Squidex.Infrastructure/Log/JsonLogWriterFactory.cs b/backend/src/Squidex.Infrastructure/Log/JsonLogWriterFactory.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/JsonLogWriterFactory.cs rename to backend/src/Squidex.Infrastructure/Log/JsonLogWriterFactory.cs diff --git a/backend/src/Squidex.Infrastructure/Log/LockingLogStore.cs b/backend/src/Squidex.Infrastructure/Log/LockingLogStore.cs new file mode 100644 index 000000000..069e8329c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/LockingLogStore.cs @@ -0,0 +1,83 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Infrastructure.Log +{ + public sealed class LockingLogStore : ILogStore + { + private static readonly byte[] LockedText = Encoding.UTF8.GetBytes("Another process is currenty running, try it again later."); + private static readonly TimeSpan LockWaitingTime = TimeSpan.FromMinutes(1); + private readonly ILogStore inner; + private readonly ILockGrain lockGrain; + + public LockingLogStore(ILogStore inner, IGrainFactory grainFactory) + { + Guard.NotNull(inner); + Guard.NotNull(grainFactory); + + this.inner = inner; + + lockGrain = grainFactory.GetGrain(SingleGrain.Id); + } + + public Task ReadLogAsync(string key, DateTime from, DateTime to, Stream stream) + { + return ReadLogAsync(key, from, to, stream, LockWaitingTime); + } + + public async Task ReadLogAsync(string key, DateTime from, DateTime to, Stream stream, TimeSpan lockTimeout) + { + using (var cts = new CancellationTokenSource(lockTimeout)) + { + string? releaseToken = null; + + while (!cts.IsCancellationRequested) + { + releaseToken = await lockGrain.AcquireLockAsync(key); + + if (releaseToken != null) + { + break; + } + + try + { + await Task.Delay(2000, cts.Token); + } + catch (OperationCanceledException) + { + break; + } + } + + if (releaseToken != null) + { + try + { + await inner.ReadLogAsync(key, from, to, stream); + } + finally + { + await lockGrain.ReleaseLockAsync(releaseToken); + } + } + else + { + await stream.WriteAsync(LockedText, 0, LockedText.Length); + } + } + } + } +} diff --git a/src/Squidex.Infrastructure/Log/NoopDisposable.cs b/backend/src/Squidex.Infrastructure/Log/NoopDisposable.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/NoopDisposable.cs rename to backend/src/Squidex.Infrastructure/Log/NoopDisposable.cs diff --git a/src/Squidex.Infrastructure/Log/NoopLogStore.cs b/backend/src/Squidex.Infrastructure/Log/NoopLogStore.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/NoopLogStore.cs rename to backend/src/Squidex.Infrastructure/Log/NoopLogStore.cs diff --git a/backend/src/Squidex.Infrastructure/Log/Profiler.cs b/backend/src/Squidex.Infrastructure/Log/Profiler.cs new file mode 100644 index 000000000..87b36515b --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/Profiler.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Log +{ + public delegate void ProfilerStarted(ProfilerSpan span); + + public static class Profiler + { + private static readonly AsyncLocal LocalSession = new AsyncLocal(); + private static readonly AsyncLocalCleaner Cleaner; + + public static ProfilerSession? Session + { + get { return LocalSession.Value; } + } + + public static event ProfilerStarted SpanStarted; + + static Profiler() + { + Cleaner = new AsyncLocalCleaner(LocalSession); + } + + public static IDisposable StartSession() + { + LocalSession.Value = new ProfilerSession(); + + return Cleaner; + } + + public static IDisposable TraceMethod(Type type, [CallerMemberName] string? memberName = null) + { + return Trace($"{type.Name}/{memberName}"); + } + + public static IDisposable TraceMethod([CallerMemberName] string? memberName = null) + { + return Trace($"{typeof(T).Name}/{memberName}"); + } + + public static IDisposable TraceMethod(string objectName, [CallerMemberName] string? memberName = null) + { + return Trace($"{objectName}/{memberName}"); + } + + public static IDisposable Trace(string key) + { + Guard.NotNull(key); + + var session = LocalSession.Value; + + if (session == null) + { + return NoopDisposable.Instance; + } + + var span = new ProfilerSpan(session, key); + + SpanStarted?.Invoke(span); + + return span; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Log/ProfilerSession.cs b/backend/src/Squidex.Infrastructure/Log/ProfilerSession.cs new file mode 100644 index 000000000..e08add22d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/ProfilerSession.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Concurrent; + +namespace Squidex.Infrastructure.Log +{ + public sealed class ProfilerSession + { + private struct ProfilerItem + { + public long Total; + public long Count; + } + + private readonly ConcurrentDictionary traces = new ConcurrentDictionary(); + + public void Measured(string name, long elapsed) + { + Guard.NotNullOrEmpty(name); + + traces.AddOrUpdate(name, x => + { + return new ProfilerItem { Total = elapsed, Count = 1 }; + }, + (x, result) => + { + result.Total += elapsed; + result.Count++; + + return result; + }); + } + + public void Write(IObjectWriter writer) + { + Guard.NotNull(writer); + + if (traces.Count > 0) + { + writer.WriteObject("profiler", p => + { + foreach (var kvp in traces) + { + p.WriteObject(kvp.Key, kvp.Value, (value, k) => k + .WriteProperty("elapsedMsTotal", value.Total) + .WriteProperty("elapsedMsAvg", value.Total / value.Count) + .WriteProperty("count", value.Count)); + } + }); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Log/ProfilerSpan.cs b/backend/src/Squidex.Infrastructure/Log/ProfilerSpan.cs new file mode 100644 index 000000000..a47b877a4 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/ProfilerSpan.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Log +{ + public sealed class ProfilerSpan : IDisposable + { + private readonly ProfilerSession session; + private readonly string key; + private ValueStopwatch watch = ValueStopwatch.StartNew(); + private List hooks; + + public string Key + { + get { return key; } + } + + public ProfilerSpan(ProfilerSession session, string key) + { + this.session = session; + + this.key = key; + } + + public void Listen(IDisposable hook) + { + Guard.NotNull(hook); + + if (hooks == null) + { + hooks = new List(1); + } + + hooks.Add(hook); + } + + public void Dispose() + { + var elapsedMs = watch.Stop(); + + session.Measured(key, elapsedMs); + + if (hooks != null) + { + for (var i = 0; i < hooks.Count; i++) + { + try + { + hooks[i].Dispose(); + } + catch + { + continue; + } + } + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Log/SemanticLog.cs b/backend/src/Squidex.Infrastructure/Log/SemanticLog.cs new file mode 100644 index 000000000..b168e11c3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/SemanticLog.cs @@ -0,0 +1,98 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Squidex.Infrastructure.Log +{ + public sealed class SemanticLog : ISemanticLog + { + private readonly ILogChannel[] channels; + private readonly ILogAppender[] appenders; + private readonly IObjectWriterFactory writerFactory; + + public SemanticLog( + IEnumerable channels, + IEnumerable appenders, + IObjectWriterFactory writerFactory) + { + Guard.NotNull(channels); + Guard.NotNull(appenders); + Guard.NotNull(writerFactory); + + this.channels = channels.ToArray(); + this.appenders = appenders.ToArray(); + this.writerFactory = writerFactory; + } + + public void Log(SemanticLogLevel logLevel, T context, Action action) + { + Guard.NotNull(action); + + var formattedText = FormatText(logLevel, context, action); + + LogFormattedText(logLevel, formattedText); + } + + private void LogFormattedText(SemanticLogLevel logLevel, string formattedText) + { + List? exceptions = null; + + for (var i = 0; i < channels.Length; i++) + { + try + { + channels[i].Log(logLevel, formattedText); + } + catch (Exception ex) + { + if (exceptions == null) + { + exceptions = new List(); + } + + exceptions.Add(ex); + } + } + + if (exceptions != null && exceptions.Count > 0) + { + throw new AggregateException("An error occurred while writing to logger(s).", exceptions); + } + } + + private string FormatText(SemanticLogLevel logLevel, T context, Action objectWriter) + { + var writer = writerFactory.Create(); + + try + { + writer.WriteProperty(nameof(logLevel), logLevel.ToString()); + + objectWriter(context, writer); + + for (var i = 0; i < appenders.Length; i++) + { + appenders[i].Append(writer, logLevel); + } + + return writer.ToString(); + } + finally + { + writerFactory.Release(writer); + } + } + + public ISemanticLog CreateScope(Action objectWriter) + { + return new SemanticLog(channels, appenders.Union(new ILogAppender[] { new ConstantsLogWriter(objectWriter) }).ToArray(), writerFactory); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs b/backend/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs new file mode 100644 index 000000000..606097fe6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs @@ -0,0 +1,194 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Log +{ + public static class SemanticLogExtensions + { + public static void LogTrace(this ISemanticLog log, Action objectWriter) + { + log.Log(SemanticLogLevel.Trace, None.Value, (_, w) => objectWriter(w)); + } + + public static void LogTrace(this ISemanticLog log, T context, Action objectWriter) + { + log.Log(SemanticLogLevel.Trace, context, objectWriter); + } + + public static void LogDebug(this ISemanticLog log, Action objectWriter) + { + log.Log(SemanticLogLevel.Debug, None.Value, (_, w) => objectWriter(w)); + } + + public static void LogDebug(this ISemanticLog log, T context, Action objectWriter) + { + log.Log(SemanticLogLevel.Debug, context, objectWriter); + } + + public static void LogInformation(this ISemanticLog log, Action objectWriter) + { + log.Log(SemanticLogLevel.Information, None.Value, (_, w) => objectWriter(w)); + } + + public static void LogInformation(this ISemanticLog log, T context, Action objectWriter) + { + log.Log(SemanticLogLevel.Information, context, objectWriter); + } + + public static void LogWarning(this ISemanticLog log, Action objectWriter) + { + log.Log(SemanticLogLevel.Warning, None.Value, (_, w) => objectWriter(w)); + } + + public static void LogWarning(this ISemanticLog log, T context, Action objectWriter) + { + log.Log(SemanticLogLevel.Warning, context, objectWriter); + } + + public static void LogWarning(this ISemanticLog log, Exception exception, Action? objectWriter = null) + { + log.Log(SemanticLogLevel.Warning, None.Value, (_, w) => w.WriteException(exception, objectWriter)); + } + + public static void LogWarning(this ISemanticLog log, Exception exception, T context, Action? objectWriter = null) + { + log.Log(SemanticLogLevel.Warning, context, (ctx, w) => w.WriteException(exception, ctx, objectWriter)); + } + + public static void LogError(this ISemanticLog log, Action objectWriter) + { + log.Log(SemanticLogLevel.Error, None.Value, (_, w) => objectWriter(w)); + } + + public static void LogError(this ISemanticLog log, T context, Action objectWriter) + { + log.Log(SemanticLogLevel.Error, context, objectWriter); + } + + public static void LogError(this ISemanticLog log, Exception exception, Action? objectWriter = null) + { + log.Log(SemanticLogLevel.Error, None.Value, (_, w) => w.WriteException(exception, objectWriter)); + } + + public static void LogError(this ISemanticLog log, Exception exception, T context, Action? objectWriter = null) + { + log.Log(SemanticLogLevel.Error, context, (ctx, w) => w.WriteException(exception, ctx, objectWriter)); + } + + public static void LogFatal(this ISemanticLog log, Action objectWriter) + { + log.Log(SemanticLogLevel.Fatal, None.Value, (_, w) => objectWriter(w)); + } + + public static void LogFatal(this ISemanticLog log, T context, Action objectWriter) + { + log.Log(SemanticLogLevel.Fatal, context, objectWriter); + } + + public static void LogFatal(this ISemanticLog log, Exception? exception, Action? objectWriter = null) + { + log.Log(SemanticLogLevel.Fatal, None.Value, (_, w) => w.WriteException(exception, objectWriter)); + } + + public static void LogFatal(this ISemanticLog log, Exception? exception, T context, Action? objectWriter = null) + { + log.Log(SemanticLogLevel.Fatal, context, (ctx, w) => w.WriteException(exception, ctx, objectWriter)); + } + + private static void WriteException(this IObjectWriter writer, Exception? exception, Action? objectWriter) + { + objectWriter?.Invoke(writer); + + if (exception != null) + { + writer.WriteException(exception); + } + } + + private static void WriteException(this IObjectWriter writer, Exception? exception, T context, Action? objectWriter) + { + objectWriter?.Invoke(context, writer); + + if (exception != null) + { + writer.WriteException(exception); + } + } + + public static IObjectWriter WriteException(this IObjectWriter writer, Exception? exception) + { + if (exception == null) + { + return writer; + } + + return writer.WriteObject(nameof(exception), exception, (ctx, w) => + { + w.WriteProperty("type", ctx.GetType().FullName); + + if (ctx.Message != null) + { + w.WriteProperty("message", ctx.Message); + } + + if (ctx.StackTrace != null) + { + w.WriteProperty("stackTrace", ctx.StackTrace); + } + }); + } + + public static IDisposable MeasureTrace(this ISemanticLog log, Action objectWriter) + { + return log.Measure(SemanticLogLevel.Trace, None.Value, (_, w) => objectWriter(w)); + } + + public static IDisposable MeasureTrace(this ISemanticLog log, T context, Action objectWriter) + { + return log.Measure(SemanticLogLevel.Trace, context, objectWriter); + } + + public static IDisposable MeasureDebug(this ISemanticLog log, Action objectWriter) + { + return log.Measure(SemanticLogLevel.Debug, None.Value, (_, w) => objectWriter(w)); + } + + public static IDisposable MeasureDebug(this ISemanticLog log, T context, Action objectWriter) + { + return log.Measure(SemanticLogLevel.Debug, context, objectWriter); + } + + public static IDisposable MeasureInformation(this ISemanticLog log, Action objectWriter) + { + return log.Measure(SemanticLogLevel.Information, None.Value, (_, w) => objectWriter(w)); + } + + public static IDisposable MeasureInformation(this ISemanticLog log, T context, Action objectWriter) + { + return log.Measure(SemanticLogLevel.Information, context, objectWriter); + } + + private static IDisposable Measure(this ISemanticLog log, SemanticLogLevel logLevel, T context, Action objectWriter) + { + var watch = ValueStopwatch.StartNew(); + + return new DelegateDisposable(() => + { + var elapsedMs = watch.Stop(); + + log.Log(logLevel, (Context: context, elapsedMs), (ctx, w) => + { + objectWriter?.Invoke(ctx.Context, w); + + w.WriteProperty("elapsedMs", elapsedMs); + }); + }); + } + } +} diff --git a/src/Squidex.Infrastructure/Log/SemanticLogLevel.cs b/backend/src/Squidex.Infrastructure/Log/SemanticLogLevel.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/SemanticLogLevel.cs rename to backend/src/Squidex.Infrastructure/Log/SemanticLogLevel.cs diff --git a/backend/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs b/backend/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs new file mode 100644 index 000000000..a971e529e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; + +namespace Squidex.Infrastructure.Log +{ + public sealed class TimestampLogAppender : ILogAppender + { + private readonly IClock clock; + + public TimestampLogAppender(IClock? clock = null) + { + this.clock = clock ?? SystemClock.Instance; + } + + public void Append(IObjectWriter writer, SemanticLogLevel logLevel) + { + writer.WriteProperty("timestamp", clock.GetCurrentInstant()); + } + } +} diff --git a/src/Squidex.Infrastructure/Migrations/IMigrated.cs b/backend/src/Squidex.Infrastructure/Migrations/IMigrated.cs similarity index 100% rename from src/Squidex.Infrastructure/Migrations/IMigrated.cs rename to backend/src/Squidex.Infrastructure/Migrations/IMigrated.cs diff --git a/src/Squidex.Infrastructure/Migrations/IMigration.cs b/backend/src/Squidex.Infrastructure/Migrations/IMigration.cs similarity index 100% rename from src/Squidex.Infrastructure/Migrations/IMigration.cs rename to backend/src/Squidex.Infrastructure/Migrations/IMigration.cs diff --git a/backend/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs b/backend/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs new file mode 100644 index 000000000..b714bbacb --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Migrations +{ + public interface IMigrationPath + { + (int Version, IEnumerable? Migrations) GetNext(int version); + } +} diff --git a/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs b/backend/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs similarity index 100% rename from src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs rename to backend/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs diff --git a/backend/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs b/backend/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs new file mode 100644 index 000000000..9767b903a --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.Serialization; + +namespace Squidex.Infrastructure.Migrations +{ + [Serializable] + public class MigrationFailedException : Exception + { + public string Name { get; } + + public MigrationFailedException(string name) + : base(FormatException(name)) + { + Name = name; + } + + public MigrationFailedException(string name, Exception inner) + : base(FormatException(name), inner) + { + Name = name; + } + + protected MigrationFailedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Name = info.GetString(nameof(Name))!; + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(Name), Name); + } + + private static string FormatException(string name) + { + return $"Failed to run migration '{name}'"; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs b/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs new file mode 100644 index 000000000..15a79e3a5 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs @@ -0,0 +1,101 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.Migrations +{ + public sealed class Migrator + { + private readonly ISemanticLog log; + private readonly IMigrationStatus migrationStatus; + private readonly IMigrationPath migrationPath; + + public int LockWaitMs { get; set; } = 500; + + public Migrator(IMigrationStatus migrationStatus, IMigrationPath migrationPath, ISemanticLog log) + { + Guard.NotNull(migrationStatus); + Guard.NotNull(migrationPath); + Guard.NotNull(log); + + this.migrationStatus = migrationStatus; + this.migrationPath = migrationPath; + + this.log = log; + } + + public async Task MigrateAsync(CancellationToken ct = default) + { + var version = 0; + + try + { + while (!await migrationStatus.TryLockAsync()) + { + log.LogInformation(w => w + .WriteProperty("action", "Migrate") + .WriteProperty("mesage", $"Waiting {LockWaitMs}ms to acquire lock.")); + + await Task.Delay(LockWaitMs); + } + + version = await migrationStatus.GetVersionAsync(); + + while (!ct.IsCancellationRequested) + { + var (newVersion, migrations) = migrationPath.GetNext(version); + + if (migrations == null || !migrations.Any()) + { + break; + } + + foreach (var migration in migrations) + { + var name = migration.GetType().ToString(); + + log.LogInformation(w => w + .WriteProperty("action", "Migration") + .WriteProperty("status", "Started") + .WriteProperty("migrator", name)); + + try + { + using (log.MeasureInformation(w => w + .WriteProperty("action", "Migration") + .WriteProperty("status", "Completed") + .WriteProperty("migrator", name))) + { + await migration.UpdateAsync(); + } + } + catch (Exception ex) + { + log.LogFatal(ex, w => w + .WriteProperty("action", "Migration") + .WriteProperty("status", "Failed") + .WriteProperty("migrator", name)); + + throw new MigrationFailedException(name, ex); + } + } + + version = newVersion; + } + } + finally + { + await migrationStatus.UnlockAsync(version); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/NamedId.cs b/backend/src/Squidex.Infrastructure/NamedId.cs new file mode 100644 index 000000000..c582a77ad --- /dev/null +++ b/backend/src/Squidex.Infrastructure/NamedId.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure +{ + public static class NamedId + { + public static NamedId Of(T id, string name) where T : notnull + { + return new NamedId(id, name); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/NamedId{T}.cs b/backend/src/Squidex.Infrastructure/NamedId{T}.cs new file mode 100644 index 000000000..42f15e61d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/NamedId{T}.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics.CodeAnalysis; + +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Infrastructure +{ + public delegate bool Parser(string input, out T result); + + public sealed class NamedId : IEquatable> where T : notnull + { + private static readonly int GuidLength = Guid.Empty.ToString().Length; + + public T Id { get; } + + public string Name { get; } + + public NamedId(T id, string name) + { + Guard.NotNull(id); + Guard.NotNull(name); + + Id = id; + + Name = name; + } + + public override string ToString() + { + return $"{Id},{Name}"; + } + + public override bool Equals(object? obj) + { + return Equals(obj as NamedId); + } + + public bool Equals(NamedId? other) + { + return other != null && (ReferenceEquals(this, other) || (Id.Equals(other.Id) && Name.Equals(other.Name))); + } + + public override int GetHashCode() + { + return (Id.GetHashCode() * 397) ^ Name.GetHashCode(); + } + + public static bool TryParse(string value, Parser parser, [MaybeNullWhen(false)] out NamedId result) + { + if (value != null) + { + if (typeof(T) == typeof(Guid)) + { + if (value.Length > GuidLength + 1 && value[GuidLength] == ',') + { + if (parser(value.Substring(0, GuidLength), out var id)) + { + result = new NamedId(id, value.Substring(GuidLength + 1)); + + return true; + } + } + } + else + { + var index = value.IndexOf(','); + + if (index > 0 && index < value.Length - 1) + { + if (parser(value.Substring(0, index), out var id)) + { + result = new NamedId(id, value.Substring(index + 1)); + + return true; + } + } + } + } + + result = null!; + + return false; + } + + public static NamedId Parse(string value, Parser parser) + { + if (!TryParse(value, parser, out var result)) + { + throw new ArgumentException("Named id must have at least 2 parts divided by commata.", nameof(value)); + } + + return result; + } + } +} diff --git a/src/Squidex.Infrastructure/Net/IPAddressComparer.cs b/backend/src/Squidex.Infrastructure/Net/IPAddressComparer.cs similarity index 100% rename from src/Squidex.Infrastructure/Net/IPAddressComparer.cs rename to backend/src/Squidex.Infrastructure/Net/IPAddressComparer.cs diff --git a/backend/src/Squidex.Infrastructure/None.cs b/backend/src/Squidex.Infrastructure/None.cs new file mode 100644 index 000000000..5f1564823 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/None.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure +{ + public sealed class None + { + public static readonly Type Type = typeof(None); + + public static readonly None Value = new None(); + + private None() + { + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs b/backend/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs new file mode 100644 index 000000000..f4f5e890c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.DependencyInjection; +using Orleans; +using Orleans.Runtime; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class ActivationLimit : IActivationLimit, IDeactivater + { + private readonly IGrainActivationContext context; + private readonly IActivationLimiter limiter; + private int maxActivations; + + public ActivationLimit(IGrainActivationContext context, IActivationLimiter limiter) + { + Guard.NotNull(context); + Guard.NotNull(limiter); + + this.context = context; + this.limiter = limiter; + } + + public void ReportIAmAlive() + { + if (maxActivations > 0) + { + limiter.Register(context.GrainType, this, maxActivations); + } + } + + public void ReportIAmDead() + { + if (maxActivations > 0) + { + limiter.Unregister(context.GrainType, this); + } + } + + public void SetLimit(int activations, TimeSpan lifetime) + { + maxActivations = activations; + + context.ObservableLifecycle?.Subscribe("Limiter", GrainLifecycleStage.Activate, + ct => + { + var runtime = context.ActivationServices.GetRequiredService(); + + runtime.DelayDeactivation(context.GrainInstance, lifetime); + + ReportIAmAlive(); + + return TaskHelper.Done; + }, + ct => + { + ReportIAmDead(); + + return TaskHelper.Done; + }); + } + + void IDeactivater.Deactivate() + { + var runtime = context.ActivationServices.GetRequiredService(); + + runtime.DeactivateOnIdle(context.GrainInstance); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs b/backend/src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs rename to backend/src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs diff --git a/src/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs rename to backend/src/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs diff --git a/backend/src/Squidex.Infrastructure/Orleans/GrainBase.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainBase.cs new file mode 100644 index 000000000..e8b940255 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/GrainBase.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.DependencyInjection; +using Orleans; +using Orleans.Core; +using Orleans.Runtime; + +namespace Squidex.Infrastructure.Orleans +{ + public abstract class GrainBase : Grain + { + protected GrainBase() + { + } + + protected GrainBase(IGrainIdentity? identity, IGrainRuntime? runtime) + : base(identity, runtime) + { + } + + public void ReportIAmAlive() + { + var limit = ServiceProvider.GetService(); + + limit?.ReportIAmAlive(); + } + + public void ReportIAmDead() + { + var limit = ServiceProvider.GetService(); + + limit?.ReportIAmDead(); + } + + protected void TryDelayDeactivation(TimeSpan timeSpan) + { + try + { + DelayDeactivation(timeSpan); + } + catch (InvalidOperationException) + { + } + } + + protected void TryDeactivateOnIdle() + { + try + { + DeactivateOnIdle(); + } + catch (InvalidOperationException) + { + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs new file mode 100644 index 000000000..16cc558db --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Orleans; +using Orleans.Runtime; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class GrainBootstrap : IBackgroundProcess where T : IBackgroundGrain + { + private const int NumTries = 10; + private readonly IGrainFactory grainFactory; + + public GrainBootstrap(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public async Task StartAsync(CancellationToken ct = default) + { + for (var i = 1; i <= NumTries; i++) + { + ct.ThrowIfCancellationRequested(); + try + { + var grain = grainFactory.GetGrain(SingleGrain.Id); + + await grain.ActivateAsync(); + + return; + } + catch (OrleansException) + { + if (i == NumTries) + { + throw; + } + } + } + } + + public override string ToString() + { + return typeof(T).ToString(); + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs rename to backend/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs diff --git a/src/Squidex.Infrastructure/Orleans/GrainOfString.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainOfString.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/GrainOfString.cs rename to backend/src/Squidex.Infrastructure/Orleans/GrainOfString.cs diff --git a/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs new file mode 100644 index 000000000..6c07ee7f0 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Orleans; +using Orleans.Runtime; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class GrainState : IGrainState where T : class, new() + { + private readonly IGrainActivationContext context; + private IPersistence persistence; + + public T Value { get; set; } = new T(); + + public long Version + { + get { return persistence.Version; } + } + + public GrainState(IGrainActivationContext context) + { + Guard.NotNull(context); + + this.context = context; + + context.ObservableLifecycle.Subscribe("Persistence", GrainLifecycleStage.SetupState, SetupAsync); + } + + public Task SetupAsync(CancellationToken ct = default) + { + if (ct.IsCancellationRequested) + { + return Task.CompletedTask; + } + + if (context.GrainIdentity.PrimaryKeyString != null) + { + var store = context.ActivationServices.GetService>(); + + persistence = store.WithSnapshots(GetType(), context.GrainIdentity.PrimaryKeyString, ApplyState); + } + else + { + var store = context.ActivationServices.GetService>(); + + persistence = store.WithSnapshots(GetType(), context.GrainIdentity.PrimaryKey, ApplyState); + } + + return persistence.ReadAsync(); + } + + private void ApplyState(T value) + { + Value = value; + } + + public Task ClearAsync() + { + Value = new T(); + + return persistence.DeleteAsync(); + } + + public Task WriteAsync() + { + return persistence.WriteSnapshotAsync(Value); + } + + public Task WriteEventAsync(Envelope envelope) + { + return persistence.WriteEventAsync(envelope); + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/IActivationLimit.cs b/backend/src/Squidex.Infrastructure/Orleans/IActivationLimit.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/IActivationLimit.cs rename to backend/src/Squidex.Infrastructure/Orleans/IActivationLimit.cs diff --git a/src/Squidex.Infrastructure/Orleans/IActivationLimiter.cs b/backend/src/Squidex.Infrastructure/Orleans/IActivationLimiter.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/IActivationLimiter.cs rename to backend/src/Squidex.Infrastructure/Orleans/IActivationLimiter.cs diff --git a/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs rename to backend/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs diff --git a/src/Squidex.Infrastructure/Orleans/IDeactivater.cs b/backend/src/Squidex.Infrastructure/Orleans/IDeactivater.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/IDeactivater.cs rename to backend/src/Squidex.Infrastructure/Orleans/IDeactivater.cs diff --git a/src/Squidex.Infrastructure/Orleans/IGrainState.cs b/backend/src/Squidex.Infrastructure/Orleans/IGrainState.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/IGrainState.cs rename to backend/src/Squidex.Infrastructure/Orleans/IGrainState.cs diff --git a/backend/src/Squidex.Infrastructure/Orleans/ILockGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/ILockGrain.cs new file mode 100644 index 000000000..184529d66 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/ILockGrain.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Orleans; + +namespace Squidex.Infrastructure.Orleans +{ + public interface ILockGrain : IGrainWithStringKey + { + Task AcquireLockAsync(string key); + + Task ReleaseLockAsync(string releaseToken); + } +} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs rename to backend/src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs diff --git a/backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs new file mode 100644 index 000000000..966d16cae --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public interface IUniqueNameIndexGrain + { + Task ReserveAsync(T id, string name); + + Task AddAsync(string? token); + + Task CountAsync(); + + Task RemoveReservationAsync(string? token); + + Task RemoveAsync(T id); + + Task RebuildAsync(Dictionary values); + + Task ClearAsync(); + + Task GetIdAsync(string name); + + Task> GetIdsAsync(string[] names); + + Task> GetIdsAsync(); + } +} diff --git a/backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs new file mode 100644 index 000000000..d30d9714d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public class IdsIndexGrain : Grain, IIdsIndexGrain where TState : IdsIndexState, new() + { + private readonly IGrainState state; + + public IdsIndexGrain(IGrainState state) + { + Guard.NotNull(state); + + this.state = state; + } + + public Task CountAsync() + { + return Task.FromResult(state.Value.Ids.Count); + } + + public Task RebuildAsync(HashSet ids) + { + state.Value = new TState { Ids = ids }; + + return state.WriteAsync(); + } + + public Task AddAsync(T id) + { + state.Value.Ids.Add(id); + + return state.WriteAsync(); + } + + public Task RemoveAsync(T id) + { + state.Value.Ids.Remove(id); + + return state.WriteAsync(); + } + + public Task ClearAsync() + { + return state.ClearAsync(); + } + + public Task> GetIdsAsync() + { + return Task.FromResult(state.Value.Ids.ToList()); + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs rename to backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs diff --git a/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs new file mode 100644 index 000000000..ba2afdc3d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs @@ -0,0 +1,138 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public class UniqueNameIndexGrain : Grain, IUniqueNameIndexGrain where TState : UniqueNameIndexState, new() + { + private readonly Dictionary reservations = new Dictionary(); + private readonly IGrainState state; + + public UniqueNameIndexGrain(IGrainState state) + { + Guard.NotNull(state); + + this.state = state; + } + + public Task CountAsync() + { + return Task.FromResult(state.Value.Names.Count); + } + + public Task ClearAsync() + { + reservations.Clear(); + + return state.ClearAsync(); + } + + public Task RebuildAsync(Dictionary names) + { + state.Value = new TState { Names = names }; + + return state.WriteAsync(); + } + + public Task ReserveAsync(T id, string name) + { + string? token = null; + + if (!IsInUse(name) && !IsReserved(name)) + { + token = RandomHash.Simple(); + + reservations.Add(token, (name, id)); + } + + return Task.FromResult(token); + } + + public async Task AddAsync(string? token) + { + token ??= string.Empty; + + if (reservations.TryGetValue(token, out var reservation)) + { + state.Value.Names.Add(reservation.Name, reservation.Id); + + await state.WriteAsync(); + + reservations.Remove(token); + + return true; + } + + return false; + } + + public Task RemoveReservationAsync(string? token) + { + reservations.Remove(token ?? string.Empty); + + return TaskHelper.Done; + } + + public async Task RemoveAsync(T id) + { + var name = state.Value.Names.FirstOrDefault(x => Equals(x.Value, id)).Key; + + if (name != null) + { + state.Value.Names.Remove(name); + + await state.WriteAsync(); + } + } + + public Task> GetIdsAsync(string[] names) + { + var result = new List(); + + if (names != null) + { + foreach (var name in names) + { + if (state.Value.Names.TryGetValue(name, out var id)) + { + result.Add(id); + } + } + } + + return Task.FromResult(result); + } + + public Task GetIdAsync(string name) + { + state.Value.Names.TryGetValue(name, out var id); + + return Task.FromResult(id); + } + + public Task> GetIdsAsync() + { + return Task.FromResult(state.Value.Names.Values.ToList()); + } + + private bool IsInUse(string name) + { + return state.Value.Names.ContainsKey(name); + } + + private bool IsReserved(string name) + { + return reservations.Values.Any(x => x.Name == name); + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs rename to backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs diff --git a/src/Squidex.Infrastructure/Orleans/J.cs b/backend/src/Squidex.Infrastructure/Orleans/J.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/J.cs rename to backend/src/Squidex.Infrastructure/Orleans/J.cs diff --git a/backend/src/Squidex.Infrastructure/Orleans/J{T}.cs b/backend/src/Squidex.Infrastructure/Orleans/J{T}.cs new file mode 100644 index 000000000..8584b59b5 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/J{T}.cs @@ -0,0 +1,95 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Orleans.CodeGeneration; +using Orleans.Concurrency; +using Orleans.Serialization; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; + +#pragma warning disable IDE0060 // Remove unused parameter + +namespace Squidex.Infrastructure.Orleans +{ + [Immutable] + public struct J + { + public T Value { get; } + + public J(T value) + { + Value = value; + } + + public static implicit operator T(J value) + { + return value.Value; + } + + public static implicit operator J(T d) + { + return new J(d); + } + + public override string ToString() + { + return Value?.ToString() ?? string.Empty; + } + + public static Task> AsTask(T value) + { + return Task.FromResult>(value); + } + + [CopierMethod] + public static object? Copy(object? input, ICopyContext? context) + { + return input; + } + + [SerializerMethod] + public static void Serialize(object? input, ISerializationContext context, Type? expected) + { + using (Profiler.TraceMethod(nameof(J))) + { + var jsonSerializer = GetSerializer(context); + + var stream = new StreamWriterWrapper(context.StreamWriter); + + jsonSerializer.Serialize(input, stream); + } + } + + [DeserializerMethod] + public static object? Deserialize(Type expected, IDeserializationContext context) + { + using (Profiler.TraceMethod(nameof(J))) + { + var jsonSerializer = GetSerializer(context); + + var stream = new StreamReaderWrapper(context.StreamReader); + + return jsonSerializer.Deserialize(stream, expected); + } + } + + private static IJsonSerializer GetSerializer(ISerializerContext context) + { + try + { + return context?.ServiceProvider?.GetRequiredService() ?? J.DefaultSerializer; + } + catch + { + return J.DefaultSerializer; + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs new file mode 100644 index 000000000..68700741c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Caching; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class LocalCacheFilter : IIncomingGrainCallFilter + { + private readonly ILocalCache localCache; + + public LocalCacheFilter(ILocalCache localCache) + { + Guard.NotNull(localCache); + + this.localCache = localCache; + } + + public async Task Invoke(IIncomingGrainCallContext context) + { + if (!context.Grain.GetType().Namespace!.StartsWith("Orleans", StringComparison.OrdinalIgnoreCase)) + { + using (localCache.StartContext()) + { + await context.Invoke(); + } + } + else + { + await context.Invoke(); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Orleans/LockGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/LockGrain.cs new file mode 100644 index 000000000..5c9d3f5be --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/LockGrain.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class LockGrain : GrainOfString, ILockGrain + { + private readonly Dictionary locks = new Dictionary(); + + public Task AcquireLockAsync(string key) + { + string? releaseToken = null; + + if (!locks.ContainsKey(key)) + { + releaseToken = Guid.NewGuid().ToString(); + + locks.Add(key, releaseToken); + } + + return Task.FromResult(releaseToken); + } + + public Task ReleaseLockAsync(string releaseToken) + { + var key = locks.FirstOrDefault(x => x.Value == releaseToken).Key; + + if (!string.IsNullOrWhiteSpace(key)) + { + locks.Remove(key); + } + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs new file mode 100644 index 000000000..61ff39d00 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class LoggingFilter : IIncomingGrainCallFilter + { + private readonly ISemanticLog log; + + public LoggingFilter(ISemanticLog log) + { + Guard.NotNull(log); + + this.log = log; + } + + public async Task Invoke(IIncomingGrainCallContext context) + { + try + { + await context.Invoke(); + } + catch (DomainException) + { + throw; + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "GrainInvoked") + .WriteProperty("status", "Failed") + .WriteProperty("grain", context.Grain.ToString()) + .WriteProperty("grainMethod", context.ImplementationMethod.ToString())); + + throw; + } + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/SingleGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/SingleGrain.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/SingleGrain.cs rename to backend/src/Squidex.Infrastructure/Orleans/SingleGrain.cs diff --git a/src/Squidex.Infrastructure/Orleans/StateFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/StateFilter.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/StateFilter.cs rename to backend/src/Squidex.Infrastructure/Orleans/StateFilter.cs diff --git a/backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs b/backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs new file mode 100644 index 000000000..e2d2aec5b --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs @@ -0,0 +1,88 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using Orleans.Serialization; + +namespace Squidex.Infrastructure.Orleans +{ + internal sealed class StreamReaderWrapper : Stream + { + private readonly IBinaryTokenStreamReader reader; + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get => throw new NotSupportedException(); + } + + public override long Position + { + get + { + return reader.CurrentPosition; + } + set + { + throw new NotSupportedException(); + } + } + + public StreamReaderWrapper(IBinaryTokenStreamReader reader) + { + this.reader = reader; + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + var bytesLeft = (int)(reader.Length - reader.CurrentPosition); + + if (bytesLeft < count) + { + count = bytesLeft; + } + + reader.ReadByteArray(buffer, offset, count); + + return count; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs b/backend/src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs rename to backend/src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs diff --git a/src/Squidex.Infrastructure/Plugins/IPlugin.cs b/backend/src/Squidex.Infrastructure/Plugins/IPlugin.cs similarity index 100% rename from src/Squidex.Infrastructure/Plugins/IPlugin.cs rename to backend/src/Squidex.Infrastructure/Plugins/IPlugin.cs diff --git a/src/Squidex.Infrastructure/Plugins/IWebPlugin.cs b/backend/src/Squidex.Infrastructure/Plugins/IWebPlugin.cs similarity index 100% rename from src/Squidex.Infrastructure/Plugins/IWebPlugin.cs rename to backend/src/Squidex.Infrastructure/Plugins/IWebPlugin.cs diff --git a/backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs b/backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs new file mode 100644 index 000000000..dd14f3ca3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs @@ -0,0 +1,124 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.Plugins +{ + public sealed class PluginManager + { + private readonly HashSet loadedPlugins = new HashSet(); + private readonly List<(string Plugin, string Action, Exception Exception)> exceptions = new List<(string, string, Exception)>(); + + public IReadOnlyCollection Plugins + { + get { return loadedPlugins; } + } + + public void Add(string name, Assembly assembly) + { + Guard.NotNull(assembly); + + var pluginTypes = + assembly.GetTypes() + .Where(t => typeof(IPlugin).IsAssignableFrom(t)) + .Where(t => !t.IsAbstract); + + foreach (var pluginType in pluginTypes) + { + try + { + var plugin = (IPlugin)Activator.CreateInstance(pluginType)!; + + loadedPlugins.Add(plugin); + } + catch (Exception ex) + { + LogException(name, "Instantiating", ex); + } + } + } + + public void LogException(string plugin, string action, Exception exception) + { + Guard.NotNull(plugin); + Guard.NotNull(action); + Guard.NotNull(exception); + + exceptions.Add((plugin, action, exception)); + } + + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + Guard.NotNull(services); + Guard.NotNull(config); + + foreach (var plugin in loadedPlugins) + { + plugin.ConfigureServices(services, config); + } + } + + public void ConfigureBefore(IApplicationBuilder app) + { + Guard.NotNull(app); + + foreach (var plugin in loadedPlugins.OfType()) + { + plugin.ConfigureBefore(app); + } + } + + public void ConfigureAfter(IApplicationBuilder app) + { + Guard.NotNull(app); + + foreach (var plugin in loadedPlugins.OfType()) + { + plugin.ConfigureAfter(app); + } + } + + public void Log(ISemanticLog log) + { + Guard.NotNull(log); + + if (loadedPlugins.Count > 0 || exceptions.Count > 0) + { + var status = exceptions.Count > 0 ? "CompletedWithErrors" : "Completed"; + + log.LogInformation(w => w + .WriteProperty("action", "pluginsLoaded") + .WriteProperty("status", status) + .WriteArray("errors", e => + { + foreach (var error in exceptions) + { + e.WriteObject(x => x + .WriteProperty("plugin", error.Plugin) + .WriteProperty("action", error.Action) + .WriteException(error.Exception)); + } + }) + .WriteArray("plugins", a => + { + foreach (var plugin in loadedPlugins) + { + a.WriteValue(plugin.GetType().ToString()); + } + })); + } + } + } +} diff --git a/src/Squidex.Infrastructure/Plugins/PluginOptions.cs b/backend/src/Squidex.Infrastructure/Plugins/PluginOptions.cs similarity index 100% rename from src/Squidex.Infrastructure/Plugins/PluginOptions.cs rename to backend/src/Squidex.Infrastructure/Plugins/PluginOptions.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs b/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs new file mode 100644 index 000000000..c784969bd --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs @@ -0,0 +1,87 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public static class ClrFilter + { + public static LogicalFilter And(params FilterNode[] filters) + { + return new LogicalFilter(LogicalFilterType.And, filters); + } + + public static LogicalFilter Or(params FilterNode[] filters) + { + return new LogicalFilter(LogicalFilterType.Or, filters); + } + + public static NegateFilter Not(FilterNode filter) + { + return new NegateFilter(filter); + } + + public static CompareFilter Eq(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.Equals, value); + } + + public static CompareFilter Ne(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.NotEquals, value); + } + + public static CompareFilter Lt(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.LessThan, value); + } + + public static CompareFilter Le(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.LessThanOrEqual, value); + } + + public static CompareFilter Gt(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.GreaterThan, value); + } + + public static CompareFilter Ge(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.GreaterThanOrEqual, value); + } + + public static CompareFilter Contains(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.Contains, value); + } + + public static CompareFilter EndsWith(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.EndsWith, value); + } + + public static CompareFilter StartsWith(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.StartsWith, value); + } + + public static CompareFilter Empty(PropertyPath path) + { + return Binary(path, CompareOperator.Empty, null); + } + + public static CompareFilter In(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.In, value); + } + + private static CompareFilter Binary(PropertyPath path, CompareOperator @operator, ClrValue? value) + { + return new CompareFilter(path, @operator, value ?? ClrValue.Null); + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/ClrQuery.cs b/backend/src/Squidex.Infrastructure/Queries/ClrQuery.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/ClrQuery.cs rename to backend/src/Squidex.Infrastructure/Queries/ClrQuery.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs b/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs new file mode 100644 index 000000000..8fe6ffdc0 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs @@ -0,0 +1,140 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using NodaTime; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class ClrValue + { + public static readonly ClrValue Null = new ClrValue(null, ClrValueType.Null, false); + + public object? Value { get; } + + public ClrValueType ValueType { get; } + + public bool IsList { get; } + + private ClrValue(object? value, ClrValueType valueType, bool isList) + { + Value = value; + ValueType = valueType; + + IsList = isList; + } + + public static implicit operator ClrValue(Instant value) + { + return new ClrValue(value, ClrValueType.Instant, false); + } + + public static implicit operator ClrValue(Guid value) + { + return new ClrValue(value, ClrValueType.Guid, false); + } + + public static implicit operator ClrValue(bool value) + { + return new ClrValue(value, ClrValueType.Boolean, false); + } + + public static implicit operator ClrValue(float value) + { + return new ClrValue(value, ClrValueType.Single, false); + } + + public static implicit operator ClrValue(double value) + { + return new ClrValue(value, ClrValueType.Double, false); + } + + public static implicit operator ClrValue(int value) + { + return new ClrValue(value, ClrValueType.Int32, false); + } + + public static implicit operator ClrValue(long value) + { + return new ClrValue(value, ClrValueType.Int64, false); + } + + public static implicit operator ClrValue(string? value) + { + return value != null ? new ClrValue(value, ClrValueType.String, false) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Instant, true) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Guid, true) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Boolean, true) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Single, true) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Double, true) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Int32, true) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Int64, true) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.String, true) : Null; + } + + public override string ToString() + { + if (Value is IList list) + { + return $"[{string.Join(", ", list.OfType().Select(ToString).ToArray())}]"; + } + + return ToString(Value); + } + + private static string ToString(object? value) + { + if (value == null) + { + return "null"; + } + + if (value is string s) + { + return $"'{s.Replace("'", "\\'")}'"; + } + + return string.Format(CultureInfo.InvariantCulture, "{0}", value); + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/ClrValueType.cs b/backend/src/Squidex.Infrastructure/Queries/ClrValueType.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/ClrValueType.cs rename to backend/src/Squidex.Infrastructure/Queries/ClrValueType.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs b/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs new file mode 100644 index 000000000..3f0ba063d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public sealed class CompareFilter : FilterNode + { + public PropertyPath Path { get; } + + public CompareOperator Operator { get; } + + public TValue Value { get; } + + public CompareFilter(PropertyPath path, CompareOperator @operator, TValue value) + { + Guard.NotNull(path); + Guard.NotNull(value); + Guard.Enum(@operator); + + Path = path; + + Operator = @operator; + + Value = value; + } + + public override T Accept(FilterNodeVisitor visitor) + { + return visitor.Visit(this); + } + + public override string ToString() + { + switch (Operator) + { + case CompareOperator.Contains: + return $"contains({Path}, {Value})"; + case CompareOperator.Empty: + return $"empty({Path})"; + case CompareOperator.EndsWith: + return $"endsWith({Path}, {Value})"; + case CompareOperator.StartsWith: + return $"startsWith({Path}, {Value})"; + case CompareOperator.Equals: + return $"{Path} == {Value}"; + case CompareOperator.NotEquals: + return $"{Path} != {Value}"; + case CompareOperator.GreaterThan: + return $"{Path} > {Value}"; + case CompareOperator.GreaterThanOrEqual: + return $"{Path} >= {Value}"; + case CompareOperator.LessThan: + return $"{Path} < {Value}"; + case CompareOperator.LessThanOrEqual: + return $"{Path} <= {Value}"; + case CompareOperator.In: + return $"{Path} in {Value}"; + default: + return string.Empty; + } + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Queries/CompareOperator.cs b/backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/CompareOperator.cs rename to backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/FilterNode.cs b/backend/src/Squidex.Infrastructure/Queries/FilterNode.cs new file mode 100644 index 000000000..125787f11 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/FilterNode.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public abstract class FilterNode + { + public abstract T Accept(FilterNodeVisitor visitor); + + public abstract override string ToString(); + } +} diff --git a/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs rename to backend/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs b/backend/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs new file mode 100644 index 000000000..6b7337f0e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs @@ -0,0 +1,165 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Squidex.Infrastructure.Json.Newtonsoft; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Infrastructure.Queries.Json +{ + public sealed class FilterConverter : JsonClassConverter> + { + public override IEnumerable SupportedTypes + { + get + { + yield return typeof(CompareFilter); + yield return typeof(FilterNode); + yield return typeof(LogicalFilter); + yield return typeof(NegateFilter); + } + } + + public override bool CanConvert(Type objectType) + { + return SupportedTypes.Contains(objectType); + } + + protected override FilterNode ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartObject) + { + throw new JsonException($"Expected StartObject, but got {reader.TokenType}."); + } + + FilterNode? result = null; + + PropertyPath? comparePath = null; + + var compareOperator = (CompareOperator)99; + + IJsonValue? compareValue = null; + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + var propertyName = reader.Value.ToString()!; + + if (!reader.Read()) + { + throw new JsonSerializationException("Unexpected end when reading filter."); + } + + if (result != null) + { + throw new JsonSerializationException($"Unexpected property {propertyName}"); + } + + switch (propertyName.ToLowerInvariant()) + { + case "not": + var filter = serializer.Deserialize>(reader); + + result = new NegateFilter(filter); + break; + case "and": + var andFilters = serializer.Deserialize>>(reader); + + result = new LogicalFilter(LogicalFilterType.And, andFilters); + break; + case "or": + var orFilters = serializer.Deserialize>>(reader); + + result = new LogicalFilter(LogicalFilterType.Or, orFilters); + break; + case "path": + comparePath = serializer.Deserialize(reader); + break; + case "op": + compareOperator = ReadOperator(reader, serializer); + break; + case "value": + compareValue = serializer.Deserialize(reader); + break; + } + + break; + case JsonToken.Comment: + break; + case JsonToken.EndObject: + if (result != null) + { + return result; + } + + if (comparePath == null) + { + throw new JsonSerializationException("Path not defined."); + } + + if (compareValue == null && compareOperator != CompareOperator.Empty) + { + throw new JsonSerializationException("Value not defined."); + } + + if (!compareOperator.IsEnumValue()) + { + throw new JsonSerializationException("Operator not defined."); + } + + return new CompareFilter(comparePath, compareOperator, compareValue ?? JsonValue.Null); + } + } + + throw new JsonSerializationException("Unexpected end when reading filter."); + } + + private static CompareOperator ReadOperator(JsonReader reader, JsonSerializer serializer) + { + var value = serializer.Deserialize(reader); + + switch (value.ToLowerInvariant()) + { + case "eq": + return CompareOperator.Equals; + case "ne": + return CompareOperator.NotEquals; + case "lt": + return CompareOperator.LessThan; + case "le": + return CompareOperator.LessThanOrEqual; + case "gt": + return CompareOperator.GreaterThan; + case "ge": + return CompareOperator.GreaterThanOrEqual; + case "empty": + return CompareOperator.Empty; + case "contains": + return CompareOperator.Contains; + case "endswith": + return CompareOperator.EndsWith; + case "startswith": + return CompareOperator.StartsWith; + case "in": + return CompareOperator.In; + } + + throw new JsonSerializationException($"Unexpected compare operator, got {value}."); + } + + protected override void WriteValue(JsonWriter writer, FilterNode value, JsonSerializer serializer) + { + throw new NotSupportedException(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs new file mode 100644 index 000000000..2e3192719 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using NJsonSchema; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Infrastructure.Queries.Json +{ + public sealed class JsonFilterVisitor : FilterNodeVisitor, IJsonValue> + { + private readonly List errors; + private readonly JsonSchema schema; + + private JsonFilterVisitor(JsonSchema schema, List errors) + { + this.schema = schema; + + this.errors = errors; + } + + public static FilterNode? Parse(FilterNode filter, JsonSchema schema, List errors) + { + var visitor = new JsonFilterVisitor(schema, errors); + + var parsed = filter.Accept(visitor); + + if (visitor.errors.Count > 0) + { + return null; + } + else + { + return parsed; + } + } + + public override FilterNode Visit(NegateFilter nodeIn) + { + return new NegateFilter(nodeIn.Accept(this)); + } + + public override FilterNode Visit(LogicalFilter nodeIn) + { + return new LogicalFilter(nodeIn.Type, nodeIn.Filters.Select(x => x.Accept(this)).ToList()); + } + + public override FilterNode Visit(CompareFilter nodeIn) + { + CompareFilter? result = null; + + if (nodeIn.Path.TryGetProperty(schema, errors, out var property)) + { + var isValidOperator = OperatorValidator.IsAllowedOperator(property, nodeIn.Operator); + + if (!isValidOperator) + { + errors.Add($"{nodeIn.Operator} is not a valid operator for type {property.Type} at {nodeIn.Path}."); + } + + var value = ValueConverter.Convert(property, nodeIn.Value, nodeIn.Path, errors); + + if (value != null && isValidOperator) + { + if (value.IsList && nodeIn.Operator != CompareOperator.In) + { + errors.Add($"Array value is not allowed for '{nodeIn.Operator}' operator and path '{nodeIn.Path}'."); + } + + result = new CompareFilter(nodeIn.Path, nodeIn.Operator, value); + } + } + + result ??= new CompareFilter(nodeIn.Path, nodeIn.Operator, ClrValue.Null); + + return result; + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs b/backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs rename to backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs diff --git a/src/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs b/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs rename to backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs b/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs new file mode 100644 index 000000000..bf3b1d113 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using NJsonSchema; + +namespace Squidex.Infrastructure.Queries.Json +{ + public static class PropertyPathValidator + { + public static bool TryGetProperty(this PropertyPath path, JsonSchema schema, List errors, [MaybeNullWhen(false)] out JsonSchema property) + { + foreach (var element in path) + { + var parent = schema.Reference ?? schema; + + if (parent.Properties.TryGetValue(element, out var p)) + { + schema = p; + } + else + { + if (!string.IsNullOrWhiteSpace(parent.Title)) + { + errors.Add($"'{element}' is not a property of '{parent.Title}'."); + } + else + { + errors.Add($"Path '{path}' does not point to a valid property in the model."); + } + + property = null!; + + return false; + } + } + + property = schema; + + return true; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs b/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs new file mode 100644 index 000000000..4813e90de --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs @@ -0,0 +1,76 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NJsonSchema; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Infrastructure.Queries.Json +{ + public static class QueryParser + { + public static ClrQuery Parse(this JsonSchema schema, string json, IJsonSerializer jsonSerializer) + { + var query = ParseFromJson(json, jsonSerializer); + + var result = SimpleMapper.Map(query, new ClrQuery()); + + var errors = new List(); + + ConvertSorting(schema, result, errors); + ConvertFilters(schema, result, errors, query); + + if (errors.Count > 0) + { + throw new ValidationException("Failed to parse json query", errors.Select(x => new ValidationError(x)).ToArray()); + } + + return result; + } + + private static void ConvertFilters(JsonSchema schema, ClrQuery result, List errors, Query query) + { + if (query.Filter != null) + { + var filter = JsonFilterVisitor.Parse(query.Filter, schema, errors); + + if (filter != null) + { + result.Filter = Optimizer.Optimize(filter); + } + } + } + + private static void ConvertSorting(JsonSchema schema, ClrQuery result, List errors) + { + if (result.Sort != null) + { + foreach (var sorting in result.Sort) + { + sorting.Path.TryGetProperty(schema, errors, out _); + } + } + } + + private static Query ParseFromJson(string json, IJsonSerializer jsonSerializer) + { + try + { + return jsonSerializer.Deserialize>(json); + } + catch (JsonException ex) + { + throw new ValidationException("Failed to parse json query.", new ValidationError(ex.Message)); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs b/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs new file mode 100644 index 000000000..a0318d14c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs @@ -0,0 +1,238 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NJsonSchema; +using NodaTime; +using NodaTime.Text; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Infrastructure.Queries.Json +{ + public static class ValueConverter + { + private delegate bool Parser(List errors, PropertyPath path, IJsonValue value, out T result); + + private static readonly InstantPattern[] InstantPatterns = + { + InstantPattern.General, + InstantPattern.ExtendedIso, + InstantPattern.CreateWithInvariantCulture("yyyy-MM-dd") + }; + + public static ClrValue? Convert(JsonSchema schema, IJsonValue value, PropertyPath path, List errors) + { + ClrValue? result = null; + + switch (GetType(schema)) + { + case JsonObjectType.Boolean: + { + if (value is JsonArray jsonArray) + { + result = ParseArray(errors, path, jsonArray, TryParseBoolean); + } + else if (TryParseBoolean(errors, path, value, out var temp)) + { + result = temp; + } + + break; + } + + case JsonObjectType.Integer: + case JsonObjectType.Number: + { + if (value is JsonArray jsonArray) + { + result = ParseArray(errors, path, jsonArray, TryParseNumber); + } + else if (TryParseNumber(errors, path, value, out var temp)) + { + result = temp; + } + + break; + } + + case JsonObjectType.String: + { + if (schema.Format == JsonFormatStrings.Guid) + { + if (value is JsonArray jsonArray) + { + result = ParseArray(errors, path, jsonArray, TryParseGuid); + } + else if (TryParseGuid(errors, path, value, out var temp)) + { + result = temp; + } + } + else if (schema.Format == JsonFormatStrings.DateTime) + { + if (value is JsonArray jsonArray) + { + result = ParseArray(errors, path, jsonArray, TryParseDateTime); + } + else if (TryParseDateTime(errors, path, value, out var temp)) + { + result = temp; + } + } + else + { + if (value is JsonArray jsonArray) + { + result = ParseArray(errors, path, jsonArray, TryParseString!); + } + else if (TryParseString(errors, path, value, out var temp)) + { + result = temp; + } + } + + break; + } + + default: + { + errors.Add($"Unsupported type {schema.Type} for {path}."); + break; + } + } + + return result; + } + + private static List ParseArray(List errors, PropertyPath path, JsonArray array, Parser parser) + { + var items = new List(); + + foreach (var item in array) + { + if (parser(errors, path, item, out var temp)) + { + items.Add(temp); + } + } + + return items; + } + + private static bool TryParseBoolean(List errors, PropertyPath path, IJsonValue value, out bool result) + { + result = default; + + if (value is JsonBoolean jsonBoolean) + { + result = jsonBoolean.Value; + + return true; + } + + errors.Add($"Expected Boolean for path '{path}', but got {value.Type}."); + + return false; + } + + private static bool TryParseNumber(List errors, PropertyPath path, IJsonValue value, out double result) + { + result = default; + + if (value is JsonNumber jsonNumber) + { + result = jsonNumber.Value; + + return true; + } + + errors.Add($"Expected Number for path '{path}', but got {value.Type}."); + + return false; + } + + private static bool TryParseString(List errors, PropertyPath path, IJsonValue value, out string? result) + { + result = default; + + if (value is JsonString jsonString) + { + result = jsonString.Value; + + return true; + } + else if (value is JsonNull) + { + return true; + } + + errors.Add($"Expected String for path '{path}', but got {value.Type}."); + + return false; + } + + private static bool TryParseGuid(List errors, PropertyPath path, IJsonValue value, out Guid result) + { + result = default; + + if (value is JsonString jsonString) + { + if (Guid.TryParse(jsonString.Value, out result)) + { + return true; + } + + errors.Add($"Expected Guid String for path '{path}', but got invalid String."); + } + else + { + errors.Add($"Expected Guid String for path '{path}', but got {value.Type}."); + } + + return false; + } + + private static bool TryParseDateTime(List errors, PropertyPath path, IJsonValue value, out Instant result) + { + result = default; + + if (value is JsonString jsonString) + { + foreach (var pattern in InstantPatterns) + { + var parsed = pattern.Parse(jsonString.Value); + + if (parsed.Success) + { + result = parsed.Value; + + return true; + } + } + + errors.Add($"Expected ISO8601 DateTime String for path '{path}', but got invalid String."); + } + else + { + errors.Add($"Expected ISO8601 DateTime String for path '{path}', but got {value.Type}."); + } + + return false; + } + + private static JsonObjectType GetType(JsonSchema schema) + { + if (schema.Item != null) + { + return schema.Item.Type; + } + + return schema.Type; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/LogicalFilter.cs b/backend/src/Squidex.Infrastructure/Queries/LogicalFilter.cs new file mode 100644 index 000000000..9b207e76e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/LogicalFilter.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class LogicalFilter : FilterNode + { + public IReadOnlyList> Filters { get; } + + public LogicalFilterType Type { get; } + + public LogicalFilter(LogicalFilterType type, IReadOnlyList> filters) + { + Guard.NotNull(filters); + Guard.Enum(type); + + Filters = filters; + + Type = type; + } + + public override T Accept(FilterNodeVisitor visitor) + { + return visitor.Visit(this); + } + + public override string ToString() + { + return $"({string.Join(Type == LogicalFilterType.And ? " && " : " || ", Filters)})"; + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/LogicalFilterType.cs b/backend/src/Squidex.Infrastructure/Queries/LogicalFilterType.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/LogicalFilterType.cs rename to backend/src/Squidex.Infrastructure/Queries/LogicalFilterType.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/NegateFilter.cs b/backend/src/Squidex.Infrastructure/Queries/NegateFilter.cs new file mode 100644 index 000000000..e4deb18d2 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/NegateFilter.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public sealed class NegateFilter : FilterNode + { + public FilterNode Filter { get; } + + public NegateFilter(FilterNode filter) + { + Guard.NotNull(filter); + + Filter = filter; + } + + public override T Accept(FilterNodeVisitor visitor) + { + return visitor.Visit(this); + } + + public override string ToString() + { + return $"!({Filter})"; + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs rename to backend/src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs new file mode 100644 index 000000000..54a6d5816 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs @@ -0,0 +1,178 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using Microsoft.OData; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using NodaTime; +using NodaTime.Text; + +namespace Squidex.Infrastructure.Queries.OData +{ + public sealed class ConstantWithTypeVisitor : QueryNodeVisitor + { + private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean); + private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset); + private static readonly IEdmPrimitiveType DoubleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Double); + private static readonly IEdmPrimitiveType GuidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid); + private static readonly IEdmPrimitiveType Int32Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); + private static readonly IEdmPrimitiveType Int64Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64); + private static readonly IEdmPrimitiveType SingleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Single); + private static readonly IEdmPrimitiveType StringType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String); + + private static readonly ConstantWithTypeVisitor Instance = new ConstantWithTypeVisitor(); + + private ConstantWithTypeVisitor() + { + } + + public static ClrValue Visit(QueryNode node) + { + return node.Accept(Instance); + } + + public override ClrValue Visit(ConvertNode nodeIn) + { + if (nodeIn.TypeReference.Definition == BooleanType) + { + var value = ConstantVisitor.Visit(nodeIn.Source); + + return bool.Parse(value.ToString()!); + } + + if (nodeIn.TypeReference.Definition == GuidType) + { + var value = ConstantVisitor.Visit(nodeIn.Source); + + return Guid.Parse(value.ToString()!); + } + + if (nodeIn.TypeReference.Definition == DateTimeType) + { + var value = ConstantVisitor.Visit(nodeIn.Source); + + return ParseInstant(value); + } + + if (ConstantVisitor.Visit(nodeIn.Source) == null) + { + return ClrValue.Null; + } + + throw new NotSupportedException(); + } + + public override ClrValue Visit(CollectionConstantNode nodeIn) + { + if (nodeIn.ItemType.Definition == DateTimeType) + { + return nodeIn.Collection.Select(x => ParseInstant(x.Value)).ToList(); + } + + if (nodeIn.ItemType.Definition == GuidType) + { + return nodeIn.Collection.Select(x => (Guid)x.Value).ToList(); + } + + if (nodeIn.ItemType.Definition == BooleanType) + { + return nodeIn.Collection.Select(x => (bool)x.Value).ToList(); + } + + if (nodeIn.ItemType.Definition == SingleType) + { + return nodeIn.Collection.Select(x => (float)x.Value).ToList(); + } + + if (nodeIn.ItemType.Definition == DoubleType) + { + return nodeIn.Collection.Select(x => (double)x.Value).ToList(); + } + + if (nodeIn.ItemType.Definition == Int32Type) + { + return nodeIn.Collection.Select(x => (int)x.Value).ToList(); + } + + if (nodeIn.ItemType.Definition == Int64Type) + { + return nodeIn.Collection.Select(x => (long)x.Value).ToList(); + } + + if (nodeIn.ItemType.Definition == StringType) + { + return nodeIn.Collection.Select(x => (string)x.Value).ToList(); + } + + throw new NotSupportedException(); + } + + public override ClrValue Visit(ConstantNode nodeIn) + { + if (nodeIn.TypeReference.Definition == BooleanType) + { + return (bool)nodeIn.Value; + } + + if (nodeIn.TypeReference.Definition == SingleType) + { + return (float)nodeIn.Value; + } + + if (nodeIn.TypeReference.Definition == DoubleType) + { + return (double)nodeIn.Value; + } + + if (nodeIn.TypeReference.Definition == Int32Type) + { + return (int)nodeIn.Value; + } + + if (nodeIn.TypeReference.Definition == Int64Type) + { + return (long)nodeIn.Value; + } + + if (nodeIn.TypeReference.Definition == StringType) + { + return (string)nodeIn.Value; + } + + throw new NotSupportedException(); + } + + private static Instant ParseInstant(object value) + { + if (value is DateTimeOffset dateTimeOffset) + { + return Instant.FromDateTimeOffset(dateTimeOffset.Add(dateTimeOffset.Offset)); + } + + if (value is DateTime dateTime) + { + return Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)); + } + + if (value is Date date) + { + return Instant.FromUtc(date.Year, date.Month, date.Day, 0, 0); + } + + var parseResult = InstantPattern.General.Parse(value.ToString()); + + if (!parseResult.Success) + { + throw new ODataException("Datetime is not in a valid format. Use ISO 8601"); + } + + return parseResult.Value; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs new file mode 100644 index 000000000..bf25ddb59 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace Squidex.Infrastructure.Queries.OData +{ + public static class EdmModelExtensions + { + static EdmModelExtensions() + { + CustomUriFunctions.AddCustomUriFunction("empty", + new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetBoolean(false), + EdmCoreModel.Instance.GetString(true))); + } + + public static ODataUriParser? ParseQuery(this IEdmModel model, string query) + { + if (!model.EntityContainer.EntitySets().Any()) + { + return null; + } + + query ??= string.Empty; + + var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last(); + + if (query.StartsWith("?", StringComparison.Ordinal)) + { + query = query.Substring(1); + } + + var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); + + return parser; + } + + public static ClrQuery ToQuery(this ODataUriParser? parser) + { + var query = new ClrQuery(); + + if (parser != null) + { + parser.ParseTake(query); + parser.ParseSkip(query); + parser.ParseFilter(query); + parser.ParseSort(query); + } + + return query; + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs b/backend/src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs rename to backend/src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs diff --git a/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs rename to backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs diff --git a/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs b/backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs rename to backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs diff --git a/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs rename to backend/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs diff --git a/src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs rename to backend/src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs diff --git a/src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs b/backend/src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs rename to backend/src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/Optimizer.cs b/backend/src/Squidex.Infrastructure/Queries/Optimizer.cs new file mode 100644 index 000000000..1514135a4 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/Optimizer.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class Optimizer : TransformVisitor + { + private static readonly Optimizer Instance = new Optimizer(); + + private Optimizer() + { + } + + public static FilterNode? Optimize(FilterNode source) + { + return source?.Accept(Instance); + } + + public override FilterNode? Visit(LogicalFilter nodeIn) + { + var pruned = nodeIn.Filters.Select(x => x.Accept(this)!).Where(x => x != null).ToList(); + + if (pruned.Count == 1) + { + return pruned[0]; + } + + if (pruned.Count == 0) + { + return null; + } + + return new LogicalFilter(nodeIn.Type, pruned); + } + + public override FilterNode? Visit(NegateFilter nodeIn) + { + var pruned = nodeIn.Filter.Accept(this); + + if (pruned == null) + { + return null; + } + + if (pruned is CompareFilter comparison) + { + if (comparison.Operator == CompareOperator.Equals) + { + return new CompareFilter(comparison.Path, CompareOperator.NotEquals, comparison.Value); + } + + if (comparison.Operator == CompareOperator.NotEquals) + { + return new CompareFilter(comparison.Path, CompareOperator.Equals, comparison.Value); + } + } + + return new NegateFilter(pruned); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs b/backend/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs new file mode 100644 index 000000000..4bbb67ff1 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class PascalCasePathConverter : TransformVisitor + { + private static readonly PascalCasePathConverter Instance = new PascalCasePathConverter(); + + private PascalCasePathConverter() + { + } + + public static FilterNode? Transform(FilterNode node) + { + return node.Accept(Instance); + } + + public override FilterNode? Visit(CompareFilter nodeIn) + { + return new CompareFilter(nodeIn.Path.Select(x => x.ToPascalCase()).ToList(), nodeIn.Operator, nodeIn.Value); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/PropertyPath.cs b/backend/src/Squidex.Infrastructure/Queries/PropertyPath.cs new file mode 100644 index 000000000..a6bf83e07 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/PropertyPath.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class PropertyPath : ReadOnlyCollection + { + private static readonly char[] Separators = { '.', '/' }; + + public PropertyPath(IList items) + : base(items) + { + if (items.Count == 0) + { + throw new ArgumentException("Path cannot be empty.", nameof(items)); + } + } + + public static implicit operator PropertyPath(string path) + { + return new PropertyPath(path?.Split(Separators, StringSplitOptions.RemoveEmptyEntries).ToList()!); + } + + public static implicit operator PropertyPath(string[] path) + { + return new PropertyPath(path?.ToList()!); + } + + public static implicit operator PropertyPath(List path) + { + return new PropertyPath(path); + } + + public static implicit operator PropertyPath(ImmutableList path) + { + return new PropertyPath(path); + } + + public override string ToString() + { + return string.Join(".", this); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/Query.cs b/backend/src/Squidex.Infrastructure/Queries/Query.cs new file mode 100644 index 000000000..25ab0b60a --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/Query.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Queries +{ + public class Query + { + public FilterNode? Filter { get; set; } + + public string? FullText { get; set; } + + public long Skip { get; set; } + + public long Take { get; set; } = long.MaxValue; + + public List Sort { get; set; } = new List(); + + public override string ToString() + { + var parts = new List(); + + if (Filter != null) + { + parts.Add($"Filter: {Filter}"); + } + + if (FullText != null) + { + parts.Add($"FullText: '{FullText.Replace("'", "\'")}'"); + } + + if (Skip > 0) + { + parts.Add($"Skip: {Skip}"); + } + + if (Take < long.MaxValue) + { + parts.Add($"Take: {Take}"); + } + + if (Sort.Count > 0) + { + parts.Add($"Sort: {string.Join(", ", Sort)}"); + } + + return string.Join("; ", parts); + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/SortBuilder.cs b/backend/src/Squidex.Infrastructure/Queries/SortBuilder.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/SortBuilder.cs rename to backend/src/Squidex.Infrastructure/Queries/SortBuilder.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/SortNode.cs b/backend/src/Squidex.Infrastructure/Queries/SortNode.cs new file mode 100644 index 000000000..174bb7c22 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/SortNode.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public sealed class SortNode + { + public PropertyPath Path { get; } + + public SortOrder Order { get; set; } + + public SortNode(PropertyPath path, SortOrder order) + { + Guard.NotNull(path); + Guard.Enum(order); + + Path = path; + + Order = order; + } + + public override string ToString() + { + var path = string.Join(".", Path); + + return $"{path} {Order}"; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Queries/SortOrder.cs b/backend/src/Squidex.Infrastructure/Queries/SortOrder.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/SortOrder.cs rename to backend/src/Squidex.Infrastructure/Queries/SortOrder.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/TransformVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/TransformVisitor.cs new file mode 100644 index 000000000..8d6f9b311 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/TransformVisitor.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; + +namespace Squidex.Infrastructure.Queries +{ + public abstract class TransformVisitor : FilterNodeVisitor?, TValue> + { + public override FilterNode? Visit(CompareFilter nodeIn) + { + return nodeIn; + } + + public override FilterNode? Visit(LogicalFilter nodeIn) + { + var inner = nodeIn.Filters.Select(x => x.Accept(this)!).Where(x => x != null).ToList(); + + return new LogicalFilter(nodeIn.Type, inner); + } + + public override FilterNode? Visit(NegateFilter nodeIn) + { + var inner = nodeIn.Filter.Accept(this); + + if (inner == null) + { + return inner; + } + + return new NegateFilter(inner); + } + } +} diff --git a/src/Squidex.Infrastructure/RandomHash.cs b/backend/src/Squidex.Infrastructure/RandomHash.cs similarity index 100% rename from src/Squidex.Infrastructure/RandomHash.cs rename to backend/src/Squidex.Infrastructure/RandomHash.cs diff --git a/backend/src/Squidex.Infrastructure/RefToken.cs b/backend/src/Squidex.Infrastructure/RefToken.cs new file mode 100644 index 000000000..1316019ae --- /dev/null +++ b/backend/src/Squidex.Infrastructure/RefToken.cs @@ -0,0 +1,88 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Squidex.Infrastructure +{ + public sealed class RefToken : IEquatable + { + public string Type { get; } + + public string Identifier { get; } + + public bool IsClient + { + get { return string.Equals(Type, RefTokenType.Client, StringComparison.OrdinalIgnoreCase); } + } + + public bool IsSubject + { + get { return string.Equals(Type, RefTokenType.Subject, StringComparison.OrdinalIgnoreCase); } + } + + public RefToken(string type, string identifier) + { + Guard.NotNullOrEmpty(type); + Guard.NotNullOrEmpty(identifier); + + Type = type.ToLowerInvariant(); + + Identifier = identifier; + } + + public override string ToString() + { + return $"{Type}:{Identifier}"; + } + + public override bool Equals(object? obj) + { + return Equals(obj as RefToken); + } + + public bool Equals(RefToken? other) + { + return other != null && (ReferenceEquals(this, other) || (Type.Equals(other.Type) && Identifier.Equals(other.Identifier))); + } + + public override int GetHashCode() + { + return (Type.GetHashCode() * 397) ^ Identifier.GetHashCode(); + } + + public static bool TryParse(string value, [MaybeNullWhen(false)] out RefToken result) + { + if (value != null) + { + var idx = value.IndexOf(':'); + + if (idx > 0 && idx < value.Length - 1) + { + result = new RefToken(value.Substring(0, idx), value.Substring(idx + 1)); + + return true; + } + } + + result = null!; + + return false; + } + + public static RefToken Parse(string value) + { + if (!TryParse(value, out var result)) + { + throw new ArgumentException("Ref token must have more than 2 parts divided by colon.", nameof(value)); + } + + return result; + } + } +} diff --git a/src/Squidex.Infrastructure/RefTokenType.cs b/backend/src/Squidex.Infrastructure/RefTokenType.cs similarity index 100% rename from src/Squidex.Infrastructure/RefTokenType.cs rename to backend/src/Squidex.Infrastructure/RefTokenType.cs diff --git a/src/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs b/backend/src/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs similarity index 100% rename from src/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs rename to backend/src/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs diff --git a/backend/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs b/backend/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs new file mode 100644 index 000000000..314921b2c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Reflection +{ + public interface IPropertyAccessor + { + object? Get(object target); + + void Set(object target, object? value); + } +} diff --git a/src/Squidex.Infrastructure/Reflection/ITypeProvider.cs b/backend/src/Squidex.Infrastructure/Reflection/ITypeProvider.cs similarity index 100% rename from src/Squidex.Infrastructure/Reflection/ITypeProvider.cs rename to backend/src/Squidex.Infrastructure/Reflection/ITypeProvider.cs diff --git a/backend/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs b/backend/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs new file mode 100644 index 000000000..9d3e2756b --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; + +namespace Squidex.Infrastructure.Reflection +{ + public sealed class PropertiesTypeAccessor + { + private static readonly ConcurrentDictionary AccessorCache = new ConcurrentDictionary(); + private readonly Dictionary accessors = new Dictionary(); + private readonly List properties = new List(); + + public IEnumerable Properties + { + get + { + return properties; + } + } + + private PropertiesTypeAccessor(Type type) + { + var allProperties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); + + foreach (var property in allProperties) + { + accessors[property.Name] = new PropertyAccessor(type, property); + + properties.Add(property); + } + } + + public static PropertiesTypeAccessor Create(Type targetType) + { + Guard.NotNull(targetType); + + return AccessorCache.GetOrAdd(targetType, x => new PropertiesTypeAccessor(x)); + } + + public void SetValue(object target, string propertyName, object? value) + { + Guard.NotNull(target); + + var accessor = FindAccessor(propertyName); + + accessor.Set(target, value); + } + + public object? GetValue(object target, string propertyName) + { + Guard.NotNull(target); + + var accessor = FindAccessor(propertyName); + + return accessor.Get(target); + } + + private IPropertyAccessor FindAccessor(string propertyName) + { + Guard.NotNullOrEmpty(propertyName); + + if (!accessors.TryGetValue(propertyName, out var accessor)) + { + throw new ArgumentException("Property does not exist.", nameof(propertyName)); + } + + return accessor; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs b/backend/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs new file mode 100644 index 000000000..e73ce8816 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs @@ -0,0 +1,76 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Reflection; + +namespace Squidex.Infrastructure.Reflection +{ + public sealed class PropertyAccessor : IPropertyAccessor + { + private sealed class PropertyWrapper : IPropertyAccessor + { + private readonly Func getMethod; + private readonly Action setMethod; + + public PropertyWrapper(PropertyInfo propertyInfo) + { + if (propertyInfo.CanRead) + { + getMethod = (Func)propertyInfo.GetGetMethod(true)!.CreateDelegate(typeof(Func)); + } + else + { + getMethod = x => throw new NotSupportedException(); + } + + if (propertyInfo.CanWrite) + { + setMethod = (Action)propertyInfo.GetSetMethod(true)!.CreateDelegate(typeof(Action)); + } + else + { + setMethod = (x, y) => throw new NotSupportedException(); + } + } + + public object? Get(object source) + { + return getMethod((TObject)source); + } + + public void Set(object source, object? value) + { + setMethod((TObject)source, (TValue)value!); + } + } + + private readonly IPropertyAccessor internalAccessor; + + public PropertyAccessor(Type targetType, PropertyInfo propertyInfo) + { + Guard.NotNull(targetType); + Guard.NotNull(propertyInfo); + + internalAccessor = (IPropertyAccessor)Activator.CreateInstance(typeof(PropertyWrapper<,>).MakeGenericType(propertyInfo.DeclaringType!, propertyInfo.PropertyType), propertyInfo)!; + } + + public object? Get(object target) + { + Guard.NotNull(target); + + return internalAccessor.Get(target); + } + + public void Set(object target, object? value) + { + Guard.NotNull(target); + + internalAccessor.Set(target, value); + } + } +} diff --git a/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs b/backend/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs rename to backend/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs diff --git a/backend/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs b/backend/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs new file mode 100644 index 000000000..4bb7acccb --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; + +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Infrastructure.Reflection +{ + public static class SimpleCopier + { + private struct PropertyMapper + { + private readonly IPropertyAccessor accessor; + private readonly Func converter; + + public PropertyMapper(IPropertyAccessor accessor, Func converter) + { + this.accessor = accessor; + this.converter = converter; + } + + public void MapProperty(object source, object target) + { + var value = converter(accessor.Get(source)); + + accessor.Set(target, value); + } + } + + private static class ClassCopier where T : class, new() + { + private static readonly List Mappers = new List(); + + static ClassCopier() + { + var type = typeof(T); + + foreach (var property in type.GetPublicProperties()) + { + if (!property.CanWrite || !property.CanRead) + { + continue; + } + + var accessor = new PropertyAccessor(type, property); + + if (property.PropertyType.Implements()) + { + Mappers.Add(new PropertyMapper(accessor, x => ((ICloneable)x!)?.Clone())); + } + else + { + Mappers.Add(new PropertyMapper(accessor, x => x)); + } + } + } + + public static T CopyThis(T source) + { + var destination = new T(); + + foreach (var mapper in Mappers) + { + mapper.MapProperty(source, destination); + } + + return destination; + } + } + + public static T Copy(this T source) where T : class, new() + { + Guard.NotNull(source); + + return ClassCopier.CopyThis(source); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs b/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs new file mode 100644 index 000000000..fd2d4d76e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs @@ -0,0 +1,186 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Infrastructure.Reflection +{ + public static class SimpleMapper + { + private sealed class StringConversionPropertyMapper : PropertyMapper + { + public StringConversionPropertyMapper( + IPropertyAccessor sourceAccessor, + IPropertyAccessor targetAccessor) + : base(sourceAccessor, targetAccessor) + { + } + + public override void MapProperty(object source, object target, CultureInfo culture) + { + var value = GetValue(source); + + SetValue(target, value?.ToString()); + } + } + + private sealed class ConversionPropertyMapper : PropertyMapper + { + private readonly Type targetType; + + public ConversionPropertyMapper( + IPropertyAccessor sourceAccessor, + IPropertyAccessor targetAccessor, + Type targetType) + : base(sourceAccessor, targetAccessor) + { + this.targetType = targetType; + } + + public override void MapProperty(object source, object target, CultureInfo culture) + { + var value = GetValue(source); + + if (value == null) + { + return; + } + + try + { + var converted = Convert.ChangeType(value, targetType, culture); + + SetValue(target, converted); + } + catch + { + return; + } + } + } + + private class PropertyMapper + { + private readonly IPropertyAccessor sourceAccessor; + private readonly IPropertyAccessor targetAccessor; + + public PropertyMapper(IPropertyAccessor sourceAccessor, IPropertyAccessor targetAccessor) + { + this.sourceAccessor = sourceAccessor; + this.targetAccessor = targetAccessor; + } + + public virtual void MapProperty(object source, object target, CultureInfo culture) + { + var value = GetValue(source); + + SetValue(target, value); + } + + protected void SetValue(object destination, object? value) + { + targetAccessor.Set(destination, value); + } + + protected object? GetValue(object source) + { + return sourceAccessor.Get(source); + } + } + + private static class ClassMapper where TSource : class where TTarget : class + { + private static readonly List Mappers = new List(); + + static ClassMapper() + { + var sourceClassType = typeof(TSource); + var sourceProperties = + sourceClassType.GetPublicProperties() + .Where(x => x.CanRead).ToList(); + + var targetClassType = typeof(TTarget); + var targetProperties = + targetClassType.GetPublicProperties() + .Where(x => x.CanWrite).ToList(); + + foreach (var sourceProperty in sourceProperties) + { + var targetProperty = targetProperties.FirstOrDefault(x => x.Name == sourceProperty.Name); + + if (targetProperty == null) + { + continue; + } + + var sourceType = sourceProperty.PropertyType; + var targetType = targetProperty.PropertyType; + + if (sourceType == targetType) + { + Mappers.Add(new PropertyMapper( + new PropertyAccessor(sourceClassType, sourceProperty), + new PropertyAccessor(targetClassType, targetProperty))); + } + else if (targetType == typeof(string)) + { + Mappers.Add(new StringConversionPropertyMapper( + new PropertyAccessor(sourceClassType, sourceProperty), + new PropertyAccessor(targetClassType, targetProperty))); + } + else if (sourceType.Implements() || targetType.Implements()) + { + Mappers.Add(new ConversionPropertyMapper( + new PropertyAccessor(sourceClassType, sourceProperty), + new PropertyAccessor(targetClassType, targetProperty), + targetType)); + } + } + } + + public static TTarget MapClass(TSource source, TTarget destination, CultureInfo culture) + { + foreach (var mapper in Mappers) + { + mapper.MapProperty(source, destination, culture); + } + + return destination; + } + } + + public static TTarget Map(TSource source) + where TSource : class + where TTarget : class, new() + { + return Map(source, new TTarget(), CultureInfo.CurrentCulture); + } + + public static TTarget Map(TSource source, TTarget target) + where TSource : class + where TTarget : class + { + return Map(source, target, CultureInfo.CurrentCulture); + } + + public static TTarget Map(TSource source, TTarget target, CultureInfo culture) + where TSource : class + where TTarget : class + { + Guard.NotNull(source); + Guard.NotNull(culture); + Guard.NotNull(target); + + return ClassMapper.MapClass(source, target, culture); + } + } +} diff --git a/src/Squidex.Infrastructure/Reflection/TypeNameAttribute.cs b/backend/src/Squidex.Infrastructure/Reflection/TypeNameAttribute.cs similarity index 100% rename from src/Squidex.Infrastructure/Reflection/TypeNameAttribute.cs rename to backend/src/Squidex.Infrastructure/Reflection/TypeNameAttribute.cs diff --git a/src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs b/backend/src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs similarity index 100% rename from src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs rename to backend/src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs diff --git a/src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs b/backend/src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs similarity index 100% rename from src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs rename to backend/src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs diff --git a/backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs b/backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs new file mode 100644 index 000000000..65d4570b9 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs @@ -0,0 +1,163 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Squidex.Infrastructure.Reflection +{ + public sealed class TypeNameRegistry + { + private readonly Dictionary namesByType = new Dictionary(); + private readonly Dictionary typesByName = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public TypeNameRegistry(IEnumerable? providers = null) + { + if (providers != null) + { + foreach (var provider in providers) + { + Map(provider); + } + } + } + + public TypeNameRegistry MapObsolete(Type type, string name) + { + Guard.NotNull(type); + Guard.NotNull(name); + + lock (namesByType) + { + if (typesByName.TryGetValue(name, out var existingType) && existingType != type) + { + var message = $"The name '{name}' is already registered with type '{typesByName[name]}'"; + + throw new ArgumentException(message, nameof(type)); + } + + typesByName[name] = type; + } + + return this; + } + + public TypeNameRegistry Map(ITypeProvider provider) + { + Guard.NotNull(provider); + + provider.Map(this); + + return this; + } + + public TypeNameRegistry Map(Type type) + { + Guard.NotNull(type); + + var typeNameAttribute = type.GetCustomAttribute(); + + if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName)) + { + Map(type, typeNameAttribute.TypeName); + } + + return this; + } + + public TypeNameRegistry Map(Type type, string name) + { + Guard.NotNull(type); + Guard.NotNull(name); + + lock (namesByType) + { + if (namesByType.TryGetValue(type, out var existingName) && existingName != name) + { + var message = $"The type '{type}' is already registered with name '{namesByType[type]}'"; + + throw new ArgumentException(message, nameof(type)); + } + + namesByType[type] = name; + + if (typesByName.TryGetValue(name, out var existingType) && existingType != type) + { + var message = $"The name '{name}' is already registered with type '{typesByName[name]}'"; + + throw new ArgumentException(message, nameof(type)); + } + + typesByName[name] = type; + } + + return this; + } + + public TypeNameRegistry MapUnmapped(Assembly assembly) + { + foreach (var type in assembly.GetTypes()) + { + if (!namesByType.ContainsKey(type)) + { + Map(type); + } + } + + return this; + } + + public string GetName() + { + return GetName(typeof(T)); + } + + public string GetNameOrNull() + { + return GetNameOrNull(typeof(T)); + } + + public string GetNameOrNull(Type type) + { + var result = namesByType.GetOrDefault(type); + + return result; + } + + public Type GetTypeOrNull(string name) + { + var result = typesByName.GetOrDefault(name); + + return result; + } + + public string GetName(Type type) + { + var result = namesByType.GetOrDefault(type); + + if (result == null) + { + throw new TypeNameNotFoundException($"There is no name for type '{type}"); + } + + return result; + } + + public Type GetType(string name) + { + var result = typesByName.GetOrDefault(name); + + if (result == null) + { + throw new TypeNameNotFoundException($"There is no type for name '{name}"); + } + + return result; + } + } +} diff --git a/src/Squidex.Infrastructure/ResultList.cs b/backend/src/Squidex.Infrastructure/ResultList.cs similarity index 100% rename from src/Squidex.Infrastructure/ResultList.cs rename to backend/src/Squidex.Infrastructure/ResultList.cs diff --git a/backend/src/Squidex.Infrastructure/RetryWindow.cs b/backend/src/Squidex.Infrastructure/RetryWindow.cs new file mode 100644 index 000000000..4f3250619 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/RetryWindow.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NodaTime; + +namespace Squidex.Infrastructure +{ + public sealed class RetryWindow + { + private readonly Duration windowDuration; + private readonly int windowSize; + private readonly Queue retries = new Queue(); + private readonly IClock clock; + + public RetryWindow(TimeSpan windowDuration, int windowSize, IClock? clock = null) + { + this.windowDuration = Duration.FromTimeSpan(windowDuration); + this.windowSize = windowSize + 1; + + this.clock = clock ?? SystemClock.Instance; + } + + public void Reset() + { + retries.Clear(); + } + + public bool CanRetryAfterFailure() + { + var now = clock.GetCurrentInstant(); + + retries.Enqueue(now); + + while (retries.Count > windowSize) + { + retries.Dequeue(); + } + + return retries.Count < windowSize || (retries.Count > 0 && (now - retries.Peek()) > windowDuration); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Security/Extensions.cs b/backend/src/Squidex.Infrastructure/Security/Extensions.cs new file mode 100644 index 000000000..bc06e33a1 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Security/Extensions.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Security.Claims; + +namespace Squidex.Infrastructure.Security +{ + public static class Extensions + { + public static RefToken? Token(this ClaimsPrincipal principal) + { + var subjectId = principal.OpenIdSubject(); + + if (!string.IsNullOrWhiteSpace(subjectId)) + { + return new RefToken(RefTokenType.Subject, subjectId); + } + + var clientId = principal.OpenIdClientId(); + + if (!string.IsNullOrWhiteSpace(clientId)) + { + return new RefToken(RefTokenType.Client, clientId); + } + + return null; + } + + public static string? OpenIdSubject(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Subject)?.Value; + } + + public static string? OpenIdClientId(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value; + } + + public static string? UserOrClientId(this ClaimsPrincipal principal) + { + return principal.OpenIdSubject() ?? principal.OpenIdClientId(); + } + + public static string? OpenIdPreferredUserName(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.PreferredUserName)?.Value; + } + + public static string? OpenIdName(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Name)?.Value; + } + + public static string? OpenIdNickName(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.NickName)?.Value; + } + + public static string? OpenIdEmail(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Email)?.Value; + } + + public static bool IsInClient(this ClaimsPrincipal principal, string client) + { + return principal.Claims.Any(x => x.Type == OpenIdClaims.ClientId && string.Equals(x.Value, client, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/src/Squidex.Infrastructure/Security/OpenIdClaims.cs b/backend/src/Squidex.Infrastructure/Security/OpenIdClaims.cs similarity index 100% rename from src/Squidex.Infrastructure/Security/OpenIdClaims.cs rename to backend/src/Squidex.Infrastructure/Security/OpenIdClaims.cs diff --git a/backend/src/Squidex.Infrastructure/Security/Permission.Part.cs b/backend/src/Squidex.Infrastructure/Security/Permission.Part.cs new file mode 100644 index 000000000..eb63501b3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Security/Permission.Part.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; + +namespace Squidex.Infrastructure.Security +{ + public sealed partial class Permission + { + internal struct Part + { + private static readonly char[] AlternativeSeparators = { '|' }; + private static readonly char[] MainSeparators = { '.' }; + + public readonly string[]? Alternatives; + + public readonly bool Exclusion; + + public Part(string[]? alternatives, bool exclusion) + { + Alternatives = alternatives; + + Exclusion = exclusion; + } + + public static Part[] ParsePath(string path) + { + var parts = path.Split(MainSeparators, StringSplitOptions.RemoveEmptyEntries); + + var result = new Part[parts.Length]; + + for (var i = 0; i < result.Length; i++) + { + result[i] = Parse(parts[i]); + } + + return result; + } + + public static Part Parse(string part) + { + var isExclusion = false; + + if (part.StartsWith(Exclude, StringComparison.OrdinalIgnoreCase)) + { + isExclusion = true; + + part = part.Substring(1); + } + + string[]? alternatives = null; + + if (part != Any) + { + alternatives = part.Split(AlternativeSeparators, StringSplitOptions.RemoveEmptyEntries); + } + + return new Part(alternatives, isExclusion); + } + + public static bool Intersects(ref Part lhs, ref Part rhs, bool allowNull) + { + if (lhs.Alternatives == null) + { + return true; + } + + if (allowNull && rhs.Alternatives == null) + { + return true; + } + + var shouldIntersect = !(lhs.Exclusion ^ rhs.Exclusion); + + return rhs.Alternatives != null && lhs.Alternatives.Intersect(rhs.Alternatives).Any() == shouldIntersect; + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Security/Permission.cs b/backend/src/Squidex.Infrastructure/Security/Permission.cs new file mode 100644 index 000000000..c24849a24 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Security/Permission.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Security +{ + public sealed partial class Permission : IComparable, IEquatable + { + public const string Any = "*"; + public const string Exclude = "^"; + + private readonly string id; + private Part[] path; + + public string Id + { + get { return id; } + } + + private Part[] Path + { + get { return path ?? (path = Part.ParsePath(id)); } + } + + public Permission(string id) + { + Guard.NotNullOrEmpty(id); + + this.id = id; + } + + public bool Allows(Permission permission) + { + if (permission == null) + { + return false; + } + + return Covers(Path, permission.Path); + } + + public bool Includes(Permission permission) + { + if (permission == null) + { + return false; + } + + return PartialCovers(Path, permission.Path); + } + + private static bool Covers(Part[] given, Part[] requested) + { + if (given.Length > requested.Length) + { + return false; + } + + for (var i = 0; i < given.Length; i++) + { + if (!Part.Intersects(ref given[i], ref requested[i], false)) + { + return false; + } + } + + return true; + } + + private static bool PartialCovers(Part[] given, Part[] requested) + { + for (var i = 0; i < Math.Min(given.Length, requested.Length); i++) + { + if (!Part.Intersects(ref given[i], ref requested[i], true)) + { + return false; + } + } + + return true; + } + + public bool StartsWith(string test) + { + return id.StartsWith(test, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object? obj) + { + return Equals(obj as Permission); + } + + public bool Equals(Permission? other) + { + return other != null && string.Equals(id, other.id, StringComparison.OrdinalIgnoreCase); + } + + public override int GetHashCode() + { + return id.GetHashCode(); + } + + public override string ToString() + { + return id; + } + + public int CompareTo(Permission other) + { + return other == null ? -1 : string.Compare(id, other.id, StringComparison.Ordinal); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Security/PermissionSet.cs b/backend/src/Squidex.Infrastructure/Security/PermissionSet.cs new file mode 100644 index 000000000..ee3c06055 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Security/PermissionSet.cs @@ -0,0 +1,91 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Squidex.Infrastructure.Security +{ + public sealed class PermissionSet : IReadOnlyCollection + { + public static readonly PermissionSet Empty = new PermissionSet(Array.Empty()); + + private readonly List permissions; + private readonly Lazy display; + + public int Count + { + get { return permissions.Count; } + } + + public PermissionSet(params Permission[] permissions) + : this((IEnumerable)permissions) + { + } + + public PermissionSet(params string[] permissions) + : this(permissions?.Select(x => new Permission(x))!) + { + } + + public PermissionSet(IEnumerable permissions) + : this(permissions?.Select(x => new Permission(x))!) + { + } + + public PermissionSet(IEnumerable permissions) + { + Guard.NotNull(permissions); + + this.permissions = permissions.ToList(); + + display = new Lazy(() => string.Join(";", this.permissions)); + } + + public bool Allows(Permission? other) + { + if (other == null) + { + return false; + } + + return permissions.Any(x => x.Allows(other)); + } + + public bool Includes(Permission? other) + { + if (other == null) + { + return false; + } + + return permissions.Any(x => x.Includes(other)); + } + + public override string ToString() + { + return display.Value; + } + + public IEnumerable ToIds() + { + return permissions.Select(x => x.Id); + } + + public IEnumerator GetEnumerator() + { + return permissions.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return permissions.GetEnumerator(); + } + } +} diff --git a/src/Squidex.Infrastructure/Singletons.cs b/backend/src/Squidex.Infrastructure/Singletons.cs similarity index 100% rename from src/Squidex.Infrastructure/Singletons.cs rename to backend/src/Squidex.Infrastructure/Singletons.cs diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj new file mode 100644 index 000000000..7248a0b61 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -0,0 +1,46 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/src/Squidex.Infrastructure/SquidexInfrastructure.cs b/backend/src/Squidex.Infrastructure/SquidexInfrastructure.cs similarity index 100% rename from src/Squidex.Infrastructure/SquidexInfrastructure.cs rename to backend/src/Squidex.Infrastructure/SquidexInfrastructure.cs diff --git a/src/Squidex.Infrastructure/States/CollectionNameAttribute.cs b/backend/src/Squidex.Infrastructure/States/CollectionNameAttribute.cs similarity index 100% rename from src/Squidex.Infrastructure/States/CollectionNameAttribute.cs rename to backend/src/Squidex.Infrastructure/States/CollectionNameAttribute.cs diff --git a/backend/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs b/backend/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs new file mode 100644 index 000000000..203dc924e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs @@ -0,0 +1,45 @@ + // ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Infrastructure.States +{ + public sealed class DefaultStreamNameResolver : IStreamNameResolver + { + private static readonly string[] Suffixes = { "Grain", "DomainObject", "State" }; + + public string GetStreamName(Type aggregateType, string id) + { + Guard.NotNullOrEmpty(id); + Guard.NotNull(aggregateType); + + return $"{aggregateType.TypeName(true, Suffixes)}-{id}"; + } + + public string WithNewId(string streamName, Func idGenerator) + { + Guard.NotNullOrEmpty(streamName); + Guard.NotNull(idGenerator); + + var positionOfDash = streamName.IndexOf('-'); + + if (positionOfDash >= 0) + { + var newId = idGenerator(streamName.Substring(positionOfDash + 1)); + + if (!string.IsNullOrWhiteSpace(newId)) + { + streamName = $"{streamName.Substring(0, positionOfDash)}-{newId}"; + } + } + + return streamName; + } + } +} diff --git a/src/Squidex.Infrastructure/States/IPersistence.cs b/backend/src/Squidex.Infrastructure/States/IPersistence.cs similarity index 100% rename from src/Squidex.Infrastructure/States/IPersistence.cs rename to backend/src/Squidex.Infrastructure/States/IPersistence.cs diff --git a/src/Squidex.Infrastructure/States/IPersistence{TState}.cs b/backend/src/Squidex.Infrastructure/States/IPersistence{TState}.cs similarity index 100% rename from src/Squidex.Infrastructure/States/IPersistence{TState}.cs rename to backend/src/Squidex.Infrastructure/States/IPersistence{TState}.cs diff --git a/src/Squidex.Infrastructure/States/ISnapshotStore.cs b/backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs similarity index 100% rename from src/Squidex.Infrastructure/States/ISnapshotStore.cs rename to backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs diff --git a/backend/src/Squidex.Infrastructure/States/IStore.cs b/backend/src/Squidex.Infrastructure/States/IStore.cs new file mode 100644 index 000000000..892770065 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/IStore.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Infrastructure.States +{ + public delegate void HandleEvent(Envelope @event); + + public delegate void HandleSnapshot(T state); + + public interface IStore + { + IPersistence WithEventSourcing(Type owner, TKey key, HandleEvent? applyEvent); + + IPersistence WithSnapshots(Type owner, TKey key, HandleSnapshot? applySnapshot); + + IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, HandleSnapshot? applySnapshot, HandleEvent? applyEvent); + + ISnapshotStore GetSnapshotStore(); + } +} diff --git a/backend/src/Squidex.Infrastructure/States/IStreamNameResolver.cs b/backend/src/Squidex.Infrastructure/States/IStreamNameResolver.cs new file mode 100644 index 000000000..cd2fe904e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/IStreamNameResolver.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.States +{ + public interface IStreamNameResolver + { + string GetStreamName(Type aggregateType, string id); + + string WithNewId(string streamName, Func idGenerator); + } +} diff --git a/backend/src/Squidex.Infrastructure/States/InconsistentStateException.cs b/backend/src/Squidex.Infrastructure/States/InconsistentStateException.cs new file mode 100644 index 000000000..96ea73b9f --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/InconsistentStateException.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.Serialization; + +namespace Squidex.Infrastructure.States +{ + [Serializable] + public class InconsistentStateException : Exception + { + private readonly long currentVersion; + private readonly long expectedVersion; + + public long CurrentVersion + { + get { return currentVersion; } + } + + public long ExpectedVersion + { + get { return expectedVersion; } + } + + public InconsistentStateException(long currentVersion, long expectedVersion, Exception? inner = null) + : base(FormatMessage(currentVersion, expectedVersion), inner) + { + this.currentVersion = currentVersion; + + this.expectedVersion = expectedVersion; + } + + protected InconsistentStateException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + currentVersion = info.GetInt64(nameof(currentVersion)); + + expectedVersion = info.GetInt64(nameof(expectedVersion)); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(currentVersion), currentVersion); + info.AddValue(nameof(expectedVersion), expectedVersion); + + base.GetObjectData(info, context); + } + + private static string FormatMessage(long currentVersion, long expectedVersion) + { + return $"Requested version {expectedVersion}, but found {currentVersion}."; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/States/Persistence.cs b/backend/src/Squidex.Infrastructure/States/Persistence.cs new file mode 100644 index 000000000..e388cf7be --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/Persistence.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Infrastructure.States +{ + internal sealed class Persistence : Persistence, IPersistence where TKey : notnull + { + public Persistence(TKey ownerKey, Type ownerType, + IEventStore eventStore, + IEventEnricher eventEnricher, + IEventDataFormatter eventDataFormatter, + ISnapshotStore snapshotStore, + IStreamNameResolver streamNameResolver, + HandleEvent? applyEvent) + : base(ownerKey, ownerType, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent) + { + } + } +} diff --git a/src/Squidex.Infrastructure/States/PersistenceMode.cs b/backend/src/Squidex.Infrastructure/States/PersistenceMode.cs similarity index 100% rename from src/Squidex.Infrastructure/States/PersistenceMode.cs rename to backend/src/Squidex.Infrastructure/States/PersistenceMode.cs diff --git a/backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs b/backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs new file mode 100644 index 000000000..6f828189c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs @@ -0,0 +1,241 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +#pragma warning disable RECS0012 // 'if' statement can be re-written as 'switch' statement + +namespace Squidex.Infrastructure.States +{ + internal class Persistence : IPersistence where TKey : notnull + { + private readonly TKey ownerKey; + private readonly Type ownerType; + private readonly ISnapshotStore snapshotStore; + private readonly IStreamNameResolver streamNameResolver; + private readonly IEventStore eventStore; + private readonly IEventEnricher eventEnricher; + private readonly IEventDataFormatter eventDataFormatter; + private readonly PersistenceMode persistenceMode; + private readonly HandleSnapshot? applyState; + private readonly HandleEvent? applyEvent; + private long versionSnapshot = EtagVersion.Empty; + private long versionEvents = EtagVersion.Empty; + private long version; + + public long Version + { + get { return version; } + } + + public Persistence(TKey ownerKey, Type ownerType, + IEventStore eventStore, + IEventEnricher eventEnricher, + IEventDataFormatter eventDataFormatter, + ISnapshotStore snapshotStore, + IStreamNameResolver streamNameResolver, + PersistenceMode persistenceMode, + HandleSnapshot? applyState, + HandleEvent? applyEvent) + { + this.ownerKey = ownerKey; + this.ownerType = ownerType; + this.applyState = applyState; + this.applyEvent = applyEvent; + this.eventStore = eventStore; + this.eventEnricher = eventEnricher; + this.eventDataFormatter = eventDataFormatter; + this.persistenceMode = persistenceMode; + this.snapshotStore = snapshotStore; + this.streamNameResolver = streamNameResolver; + } + + public async Task ReadAsync(long expectedVersion = EtagVersion.Any) + { + versionSnapshot = EtagVersion.Empty; + versionEvents = EtagVersion.Empty; + + await ReadSnapshotAsync(); + await ReadEventsAsync(); + + UpdateVersion(); + + if (expectedVersion > EtagVersion.Any && expectedVersion != version) + { + if (version == EtagVersion.Empty) + { + throw new DomainObjectNotFoundException(ownerKey.ToString()!, ownerType); + } + else + { + throw new InconsistentStateException(version, expectedVersion); + } + } + } + + private async Task ReadSnapshotAsync() + { + if (UseSnapshots()) + { + var (state, position) = await snapshotStore.ReadAsync(ownerKey); + + if (position < EtagVersion.Empty) + { + position = EtagVersion.Empty; + } + + versionSnapshot = position; + versionEvents = position; + + if (applyState != null && position >= 0) + { + applyState(state); + } + } + } + + private async Task ReadEventsAsync() + { + if (UseEventSourcing()) + { + var events = await eventStore.QueryAsync(GetStreamName(), versionEvents + 1); + + foreach (var @event in events) + { + versionEvents++; + + if (@event.EventStreamNumber != versionEvents) + { + throw new InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps."); + } + + var parsedEvent = ParseKnownEvent(@event); + + if (applyEvent != null && parsedEvent != null) + { + applyEvent(parsedEvent); + } + } + } + } + + public async Task WriteSnapshotAsync(TSnapshot state) + { + var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1; + + if (newVersion != versionSnapshot) + { + await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion); + + versionSnapshot = newVersion; + } + + UpdateVersion(); + } + + public async Task WriteEventsAsync(IEnumerable> events) + { + Guard.NotNull(events); + + var eventArray = events.ToArray(); + + if (eventArray.Length > 0) + { + var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any; + + var commitId = Guid.NewGuid(); + + foreach (var @event in eventArray) + { + eventEnricher.Enrich(@event, ownerKey); + } + + var eventStream = GetStreamName(); + var eventData = GetEventData(eventArray, commitId); + + try + { + await eventStore.AppendAsync(commitId, eventStream, expectedVersion, eventData); + } + catch (WrongEventVersionException ex) + { + throw new InconsistentStateException(ex.CurrentVersion, ex.ExpectedVersion, ex); + } + + versionEvents += eventArray.Length; + } + + UpdateVersion(); + } + + public async Task DeleteAsync() + { + if (UseEventSourcing()) + { + await eventStore.DeleteStreamAsync(GetStreamName()); + } + + if (UseSnapshots()) + { + await snapshotStore.RemoveAsync(ownerKey); + } + } + + private EventData[] GetEventData(Envelope[] events, Guid commitId) + { + return events.Map(x => eventDataFormatter.ToEventData(x, commitId, true)); + } + + private string GetStreamName() + { + return streamNameResolver.GetStreamName(ownerType, ownerKey.ToString()!); + } + + private bool UseSnapshots() + { + return persistenceMode == PersistenceMode.Snapshots || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; + } + + private bool UseEventSourcing() + { + return persistenceMode == PersistenceMode.EventSourcing || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; + } + + private Envelope? ParseKnownEvent(StoredEvent storedEvent) + { + try + { + return eventDataFormatter.Parse(storedEvent.Data); + } + catch (TypeNameNotFoundException) + { + return null; + } + } + + private void UpdateVersion() + { + if (persistenceMode == PersistenceMode.Snapshots) + { + version = versionSnapshot; + } + else if (persistenceMode == PersistenceMode.EventSourcing) + { + version = versionEvents; + } + else if (persistenceMode == PersistenceMode.SnapshotsAndEventSourcing) + { + version = Math.Max(versionEvents, versionSnapshot); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/States/Store.cs b/backend/src/Squidex.Infrastructure/States/Store.cs new file mode 100644 index 000000000..278df0a16 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/Store.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Infrastructure.States +{ + public sealed class Store : IStore where TKey : notnull + { + private readonly IServiceProvider services; + private readonly IStreamNameResolver streamNameResolver; + private readonly IEventStore eventStore; + private readonly IEventEnricher eventEnricher; + private readonly IEventDataFormatter eventDataFormatter; + + public Store( + IEventStore eventStore, + IEventEnricher eventEnricher, + IEventDataFormatter eventDataFormatter, + IServiceProvider services, + IStreamNameResolver streamNameResolver) + { + this.eventStore = eventStore; + this.eventEnricher = eventEnricher; + this.eventDataFormatter = eventDataFormatter; + this.services = services; + this.streamNameResolver = streamNameResolver; + } + + public IPersistence WithEventSourcing(Type owner, TKey key, HandleEvent? applyEvent) + { + return CreatePersistence(owner, key, applyEvent); + } + + public IPersistence WithSnapshots(Type owner, TKey key, HandleSnapshot? applySnapshot) + { + return CreatePersistence(owner, key, PersistenceMode.Snapshots, applySnapshot, null); + } + + public IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, HandleSnapshot? applySnapshot, HandleEvent? applyEvent) + { + return CreatePersistence(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); + } + + private IPersistence CreatePersistence(Type owner, TKey key, HandleEvent? applyEvent) + { + Guard.NotNull(key); + + var snapshotStore = GetSnapshotStore(); + + return new Persistence(key, owner, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent); + } + + private IPersistence CreatePersistence(Type owner, TKey key, PersistenceMode mode, HandleSnapshot? applySnapshot, HandleEvent? applyEvent) + { + Guard.NotNull(key); + + var snapshotStore = GetSnapshotStore(); + + return new Persistence(key, owner, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); + } + + public ISnapshotStore GetSnapshotStore() + { + return (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); + } + } +} diff --git a/src/Squidex.Infrastructure/States/StoreExtensions.cs b/backend/src/Squidex.Infrastructure/States/StoreExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/States/StoreExtensions.cs rename to backend/src/Squidex.Infrastructure/States/StoreExtensions.cs diff --git a/backend/src/Squidex.Infrastructure/StringExtensions.cs b/backend/src/Squidex.Infrastructure/StringExtensions.cs new file mode 100644 index 000000000..44f16583e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/StringExtensions.cs @@ -0,0 +1,801 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Squidex.Infrastructure +{ + public static class StringExtensions + { + private const char NullChar = (char)0; + + private static readonly Regex SlugRegex = new Regex("^[a-z0-9]+(\\-[a-z0-9]+)*$", RegexOptions.Compiled); + private static readonly Regex EmailRegex = new Regex("^[a-zA-Z0-9.!#$%&’*+\\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$", RegexOptions.Compiled); + private static readonly Regex PropertyNameRegex = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled); + + private static readonly Dictionary LowerCaseDiacritics; + private static readonly Dictionary Diacritics = new Dictionary + { + ['$'] = "dollar", + ['%'] = "percent", + ['&'] = "and", + ['<'] = "less", + ['>'] = "greater", + ['|'] = "or", + ['¢'] = "cent", + ['£'] = "pound", + ['¤'] = "currency", + ['¥'] = "yen", + ['©'] = "(c)", + ['ª'] = "a", + ['®'] = "(r)", + ['º'] = "o", + ['À'] = "A", + ['Á'] = "A", + ['Â'] = "A", + ['Ã'] = "A", + ['Ä'] = "AE", + ['Å'] = "A", + ['Æ'] = "AE", + ['Ç'] = "C", + ['Ə'] = "E", + ['È'] = "E", + ['É'] = "E", + ['Ê'] = "E", + ['Ë'] = "E", + ['Ì'] = "I", + ['Í'] = "I", + ['Î'] = "I", + ['Ï'] = "I", + ['Ð'] = "D", + ['Ñ'] = "N", + ['Ò'] = "O", + ['Ó'] = "O", + ['Ô'] = "O", + ['Õ'] = "O", + ['Ö'] = "OE", + ['Ø'] = "O", + ['Ù'] = "U", + ['Ú'] = "U", + ['Û'] = "U", + ['Ü'] = "UE", + ['Ý'] = "Y", + ['Þ'] = "TH", + ['ß'] = "ss", + ['à'] = "a", + ['á'] = "a", + ['â'] = "a", + ['ã'] = "a", + ['ä'] = "ae", + ['å'] = "a", + ['æ'] = "ae", + ['ç'] = "c", + ['ə'] = "e", + ['è'] = "e", + ['é'] = "e", + ['ê'] = "e", + ['ë'] = "e", + ['ì'] = "i", + ['í'] = "i", + ['î'] = "i", + ['ï'] = "i", + ['ð'] = "d", + ['ñ'] = "n", + ['ò'] = "o", + ['ó'] = "o", + ['ô'] = "o", + ['õ'] = "o", + ['ö'] = "oe", + ['ø'] = "o", + ['ù'] = "u", + ['ú'] = "u", + ['û'] = "u", + ['ü'] = "ue", + ['ý'] = "y", + ['þ'] = "th", + ['ÿ'] = "y", + ['Ā'] = "A", + ['ā'] = "a", + ['Ă'] = "A", + ['ă'] = "a", + ['Ą'] = "A", + ['ą'] = "a", + ['Ć'] = "C", + ['ć'] = "c", + ['Č'] = "C", + ['č'] = "c", + ['Ď'] = "D", + ['ď'] = "d", + ['Đ'] = "DJ", + ['đ'] = "dj", + ['Ē'] = "E", + ['ē'] = "e", + ['Ė'] = "E", + ['ė'] = "e", + ['Ę'] = "e", + ['ę'] = "e", + ['Ě'] = "E", + ['ě'] = "e", + ['Ğ'] = "G", + ['ğ'] = "g", + ['Ģ'] = "G", + ['ģ'] = "g", + ['Ĩ'] = "I", + ['ĩ'] = "i", + ['Ī'] = "i", + ['ī'] = "i", + ['Į'] = "I", + ['į'] = "i", + ['İ'] = "I", + ['ı'] = "i", + ['Ķ'] = "k", + ['ķ'] = "k", + ['Ļ'] = "L", + ['ļ'] = "l", + ['Ľ'] = "L", + ['ľ'] = "l", + ['Ł'] = "L", + ['ł'] = "l", + ['Ń'] = "N", + ['ń'] = "n", + ['Ņ'] = "N", + ['ņ'] = "n", + ['Ň'] = "N", + ['ň'] = "n", + ['Ő'] = "O", + ['ő'] = "o", + ['Œ'] = "OE", + ['œ'] = "oe", + ['Ŕ'] = "R", + ['ŕ'] = "r", + ['Ř'] = "R", + ['ř'] = "r", + ['Ś'] = "S", + ['ś'] = "s", + ['Ş'] = "S", + ['ş'] = "s", + ['Š'] = "S", + ['š'] = "s", + ['Ţ'] = "T", + ['ţ'] = "t", + ['Ť'] = "T", + ['ť'] = "t", + ['Ũ'] = "U", + ['ũ'] = "u", + ['Ū'] = "u", + ['ū'] = "u", + ['Ů'] = "U", + ['ů'] = "u", + ['Ű'] = "U", + ['ű'] = "u", + ['Ų'] = "U", + ['ų'] = "u", + ['Ź'] = "Z", + ['ź'] = "z", + ['Ż'] = "Z", + ['ż'] = "z", + ['Ž'] = "Z", + ['ž'] = "z", + ['ƒ'] = "f", + ['Ơ'] = "O", + ['ơ'] = "o", + ['Ư'] = "U", + ['ư'] = "u", + ['Lj'] = "LJ", + ['lj'] = "lj", + ['Nj'] = "NJ", + ['nj'] = "nj", + ['Ș'] = "S", + ['ș'] = "s", + ['Ț'] = "T", + ['ț'] = "t", + ['˚'] = "o", + ['Ά'] = "A", + ['Έ'] = "E", + ['Ή'] = "H", + ['Ί'] = "I", + ['Ό'] = "O", + ['Ύ'] = "Y", + ['Ώ'] = "W", + ['ΐ'] = "i", + ['Α'] = "A", + ['Β'] = "B", + ['Γ'] = "G", + ['Δ'] = "D", + ['Ε'] = "E", + ['Ζ'] = "Z", + ['Η'] = "H", + ['Θ'] = "8", + ['Ι'] = "I", + ['Κ'] = "K", + ['Λ'] = "L", + ['Μ'] = "M", + ['Ν'] = "N", + ['Ξ'] = "3", + ['Ο'] = "O", + ['Π'] = "P", + ['Ρ'] = "R", + ['Σ'] = "S", + ['Τ'] = "T", + ['Υ'] = "Y", + ['Φ'] = "F", + ['Χ'] = "X", + ['Ψ'] = "PS", + ['Ω'] = "W", + ['Ϊ'] = "I", + ['Ϋ'] = "Y", + ['ά'] = "a", + ['έ'] = "e", + ['ή'] = "h", + ['ί'] = "i", + ['ΰ'] = "y", + ['α'] = "a", + ['β'] = "b", + ['γ'] = "g", + ['δ'] = "d", + ['ε'] = "e", + ['ζ'] = "z", + ['η'] = "h", + ['θ'] = "8", + ['ι'] = "i", + ['κ'] = "k", + ['λ'] = "l", + ['μ'] = "m", + ['ν'] = "n", + ['ξ'] = "3", + ['ο'] = "o", + ['π'] = "p", + ['ρ'] = "r", + ['ς'] = "s", + ['σ'] = "s", + ['τ'] = "t", + ['υ'] = "y", + ['φ'] = "f", + ['χ'] = "x", + ['ψ'] = "ps", + ['ω'] = "w", + ['ϊ'] = "i", + ['ϋ'] = "y", + ['ό'] = "o", + ['ύ'] = "y", + ['ώ'] = "w", + ['Ё'] = "Yo", + ['Ђ'] = "DJ", + ['Є'] = "Ye", + ['І'] = "I", + ['Ї'] = "Yi", + ['Ј'] = "J", + ['Љ'] = "LJ", + ['Њ'] = "NJ", + ['Ћ'] = "C", + ['Џ'] = "DZ", + ['А'] = "A", + ['Б'] = "B", + ['В'] = "V", + ['Г'] = "G", + ['Д'] = "D", + ['Е'] = "E", + ['Ж'] = "Zh", + ['З'] = "Z", + ['И'] = "I", + ['Й'] = "J", + ['К'] = "K", + ['Л'] = "L", + ['М'] = "M", + ['Н'] = "N", + ['О'] = "O", + ['П'] = "P", + ['Р'] = "R", + ['С'] = "S", + ['Т'] = "T", + ['У'] = "U", + ['Ф'] = "F", + ['Х'] = "H", + ['Ц'] = "C", + ['Ч'] = "Ch", + ['Ш'] = "Sh", + ['Щ'] = "Sh", + ['Ъ'] = "U", + ['Ы'] = "Y", + ['Ь'] = "b", + ['Э'] = "E", + ['Ю'] = "Yu", + ['Я'] = "Ya", + ['а'] = "a", + ['б'] = "b", + ['в'] = "v", + ['г'] = "g", + ['д'] = "d", + ['е'] = "e", + ['ж'] = "zh", + ['з'] = "z", + ['и'] = "i", + ['й'] = "j", + ['к'] = "k", + ['л'] = "l", + ['м'] = "m", + ['н'] = "n", + ['о'] = "o", + ['п'] = "p", + ['р'] = "r", + ['с'] = "s", + ['т'] = "t", + ['у'] = "u", + ['ф'] = "f", + ['х'] = "h", + ['ц'] = "c", + ['ч'] = "ch", + ['ш'] = "sh", + ['щ'] = "sh", + ['ъ'] = "u", + ['ы'] = "y", + ['ь'] = "s", + ['э'] = "e", + ['ю'] = "yu", + ['я'] = "ya", + ['ё'] = "yo", + ['ђ'] = "dj", + ['є'] = "ye", + ['і'] = "i", + ['ї'] = "yi", + ['ј'] = "j", + ['љ'] = "lj", + ['њ'] = "nj", + ['ћ'] = "c", + ['џ'] = "dz", + ['Ґ'] = "G", + ['ґ'] = "g", + ['฿'] = "baht", + ['ა'] = "a", + ['ბ'] = "b", + ['გ'] = "g", + ['დ'] = "d", + ['ე'] = "e", + ['ვ'] = "v", + ['ზ'] = "z", + ['თ'] = "t", + ['ი'] = "i", + ['კ'] = "k", + ['ლ'] = "l", + ['მ'] = "m", + ['ნ'] = "n", + ['ო'] = "o", + ['პ'] = "p", + ['ჟ'] = "zh", + ['რ'] = "r", + ['ს'] = "s", + ['ტ'] = "t", + ['უ'] = "u", + ['ფ'] = "f", + ['ქ'] = "k", + ['ღ'] = "gh", + ['ყ'] = "q", + ['შ'] = "sh", + ['ჩ'] = "ch", + ['ც'] = "ts", + ['ძ'] = "dz", + ['წ'] = "ts", + ['ჭ'] = "ch", + ['ხ'] = "kh", + ['ჯ'] = "j", + ['ჰ'] = "h", + ['ẞ'] = "SS", + ['Ạ'] = "A", + ['ạ'] = "a", + ['Ả'] = "A", + ['ả'] = "a", + ['Ấ'] = "A", + ['ấ'] = "a", + ['Ầ'] = "A", + ['ầ'] = "a", + ['Ẩ'] = "A", + ['ẩ'] = "a", + ['Ẫ'] = "A", + ['ẫ'] = "a", + ['Ậ'] = "A", + ['ậ'] = "a", + ['Ắ'] = "A", + ['ắ'] = "a", + ['Ằ'] = "A", + ['ằ'] = "a", + ['Ẳ'] = "A", + ['ẳ'] = "a", + ['Ẵ'] = "A", + ['ẵ'] = "a", + ['Ặ'] = "A", + ['ặ'] = "a", + ['Ẹ'] = "E", + ['ẹ'] = "e", + ['Ẻ'] = "E", + ['ẻ'] = "e", + ['Ẽ'] = "E", + ['ẽ'] = "e", + ['Ế'] = "E", + ['ế'] = "e", + ['Ề'] = "E", + ['ề'] = "e", + ['Ể'] = "E", + ['ể'] = "e", + ['Ễ'] = "E", + ['ễ'] = "e", + ['Ệ'] = "E", + ['ệ'] = "e", + ['Ỉ'] = "I", + ['ỉ'] = "i", + ['Ị'] = "I", + ['ị'] = "i", + ['Ọ'] = "O", + ['ọ'] = "o", + ['Ỏ'] = "O", + ['ỏ'] = "o", + ['Ố'] = "O", + ['ố'] = "o", + ['Ồ'] = "O", + ['ồ'] = "o", + ['Ổ'] = "O", + ['ổ'] = "o", + ['Ỗ'] = "O", + ['ỗ'] = "o", + ['Ộ'] = "O", + ['ộ'] = "o", + ['Ớ'] = "O", + ['ớ'] = "o", + ['Ờ'] = "O", + ['ờ'] = "o", + ['Ở'] = "O", + ['ở'] = "o", + ['Ỡ'] = "O", + ['ỡ'] = "o", + ['Ợ'] = "O", + ['ợ'] = "o", + ['Ụ'] = "U", + ['ụ'] = "u", + ['Ủ'] = "U", + ['ủ'] = "u", + ['Ứ'] = "U", + ['ứ'] = "u", + ['Ừ'] = "U", + ['ừ'] = "u", + ['Ử'] = "U", + ['ử'] = "u", + ['Ữ'] = "U", + ['ữ'] = "u", + ['Ự'] = "U", + ['ự'] = "u", + ['Ỳ'] = "Y", + ['ỳ'] = "y", + ['Ỵ'] = "Y", + ['ỵ'] = "y", + ['Ỷ'] = "Y", + ['ỷ'] = "y", + ['Ỹ'] = "Y", + ['ỹ'] = "y", + ['‘'] = "\'", + ['’'] = "\'", + ['“'] = "\\\"", + ['”'] = "\\\"", + ['†'] = "+", + ['•'] = "*", + ['…'] = "...", + ['₠'] = "ecu", + ['₢'] = "cruzeiro", + ['₣'] = "french franc", + ['₤'] = "lira", + ['₥'] = "mill", + ['₦'] = "naira", + ['₧'] = "peseta", + ['₨'] = "rupee", + ['₩'] = "won", + ['₪'] = "new shequel", + ['₫'] = "dong", + ['€'] = "euro", + ['₭'] = "kip", + ['₮'] = "tugrik", + ['₯'] = "drachma", + ['₰'] = "penny", + ['₱'] = "peso", + ['₲'] = "guarani", + ['₳'] = "austral", + ['₴'] = "hryvnia", + ['₵'] = "cedi", + ['₹'] = "indian rupee", + ['₽'] = "russian ruble", + ['₿'] = "bitcoin", + ['℠'] = "sm", + ['™'] = "tm", + ['∂'] = "d", + ['∆'] = "delta", + ['∑'] = "sum", + ['∞'] = "infinity", + ['♥'] = "love", + ['元'] = "yuan", + ['円'] = "yen", + ['﷼'] = "rial" + }; + + static StringExtensions() + { + LowerCaseDiacritics = Diacritics.ToDictionary(x => x.Key, x => x.Value.ToLowerInvariant()); + } + + public static bool IsSlug(this string? value) + { + return value != null && SlugRegex.IsMatch(value); + } + + public static bool IsEmail(this string? value) + { + return value != null && EmailRegex.IsMatch(value); + } + + public static bool IsPropertyName(this string? value) + { + return value != null && PropertyNameRegex.IsMatch(value); + } + + public static string WithFallback(this string? value, string fallback) + { + return !string.IsNullOrWhiteSpace(value) ? value.Trim() : fallback; + } + + public static string ToPascalCase(this string value) + { + if (value.Length == 0) + { + return string.Empty; + } + + var sb = new StringBuilder(value.Length); + + var last = NullChar; + var length = 0; + + for (var i = 0; i < value.Length; i++) + { + var c = value[i]; + + if (c == '-' || c == '_' || c == ' ') + { + if (last != NullChar) + { + sb.Append(char.ToUpperInvariant(last)); + } + + last = NullChar; + length = 0; + } + else + { + if (length > 1) + { + sb.Append(c); + } + else if (length == 0) + { + last = c; + } + else + { + sb.Append(char.ToUpperInvariant(last)); + sb.Append(c); + + last = NullChar; + } + + length++; + } + } + + if (last != NullChar) + { + sb.Append(char.ToUpperInvariant(last)); + } + + return sb.ToString(); + } + + public static string ToKebabCase(this string value) + { + if (value.Length == 0) + { + return string.Empty; + } + + var sb = new StringBuilder(value.Length); + + var length = 0; + + for (var i = 0; i < value.Length; i++) + { + var c = value[i]; + + if (c == '-' || c == '_' || c == ' ') + { + length = 0; + } + else + { + if (length > 0) + { + sb.Append(char.ToLowerInvariant(c)); + } + else + { + if (sb.Length > 0) + { + sb.Append('-'); + } + + sb.Append(char.ToLowerInvariant(c)); + } + + length++; + } + } + + return sb.ToString(); + } + + public static string ToCamelCase(this string value) + { + if (value.Length == 0) + { + return string.Empty; + } + + var sb = new StringBuilder(value.Length); + + var last = NullChar; + var length = 0; + + for (var i = 0; i < value.Length; i++) + { + var c = value[i]; + + if (c == '-' || c == '_' || c == ' ') + { + if (last != NullChar) + { + if (sb.Length > 0) + { + sb.Append(char.ToUpperInvariant(last)); + } + else + { + sb.Append(char.ToLowerInvariant(last)); + } + } + + last = NullChar; + length = 0; + } + else + { + if (length > 1) + { + sb.Append(c); + } + else if (length == 0) + { + last = c; + } + else + { + if (sb.Length > 0) + { + sb.Append(char.ToUpperInvariant(last)); + } + else + { + sb.Append(char.ToLowerInvariant(last)); + } + + sb.Append(c); + + last = NullChar; + } + + length++; + } + } + + if (last != NullChar) + { + if (sb.Length > 0) + { + sb.Append(char.ToUpperInvariant(last)); + } + else + { + sb.Append(char.ToLowerInvariant(last)); + } + } + + return sb.ToString(); + } + + public static string Slugify(this string value, ISet? preserveHash = null, bool singleCharDiactric = false, char separator = '-') + { + var result = new StringBuilder(value.Length); + + var lastChar = (char)0; + + for (var i = 0; i < value.Length; i++) + { + var character = value[i]; + + if (preserveHash?.Contains(character) == true) + { + result.Append(character); + } + else if (char.IsLetter(character) || char.IsNumber(character)) + { + lastChar = character; + + var lower = char.ToLowerInvariant(character); + + if (LowerCaseDiacritics.TryGetValue(character, out var replacement)) + { + if (singleCharDiactric && replacement.Length == 2) + { + result.Append(replacement[0]); + } + else + { + result.Append(replacement); + } + } + else + { + result.Append(lower); + } + } + else if ((i < value.Length - 1) && (i > 0 && lastChar != separator)) + { + lastChar = separator; + + result.Append(separator); + } + } + + return result.ToString().Trim(separator); + } + + public static string BuildFullUrl(this string baseUrl, string path, bool trailingSlash = false) + { + Guard.NotNull(path); + + var url = $"{baseUrl.TrimEnd('/')}/{path.Trim('/')}"; + + if (trailingSlash && + url.IndexOf("#", StringComparison.OrdinalIgnoreCase) < 0 && + url.IndexOf("?", StringComparison.OrdinalIgnoreCase) < 0 && + url.IndexOf(";", StringComparison.OrdinalIgnoreCase) < 0) + { + url += "/"; + } + + return url; + } + + public static string JoinNonEmpty(string separator, params string?[] parts) + { + Guard.NotNull(separator); + + if (parts == null || parts.Length == 0) + { + return string.Empty; + } + + return string.Join(separator, parts.Where(x => !string.IsNullOrWhiteSpace(x))); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs b/backend/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs new file mode 100644 index 000000000..f7127be20 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class AsyncLocalCleaner : IDisposable + { + private readonly AsyncLocal asyncLocal; + + public AsyncLocalCleaner(AsyncLocal asyncLocal) + { + Guard.NotNull(asyncLocal); + + this.asyncLocal = asyncLocal; + } + + public void Dispose() + { + asyncLocal.Value = default!; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs b/backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs new file mode 100644 index 000000000..3425a60b6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; + +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class AsyncLock + { + private readonly SemaphoreSlim semaphore; + + public AsyncLock() + { + semaphore = new SemaphoreSlim(1); + } + + public Task LockAsync() + { + var wait = semaphore.WaitAsync(); + + if (wait.IsCompleted) + { + return Task.FromResult((IDisposable)new LockReleaser(this)); + } + else + { + return wait.ContinueWith(x => (IDisposable)new LockReleaser(this), + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + } + + private class LockReleaser : IDisposable + { + private AsyncLock? target; + + internal LockReleaser(AsyncLock target) + { + this.target = target; + } + + public void Dispose() + { + var current = target; + + if (current == null) + { + return; + } + + target = null; + + try + { + current.semaphore.Release(); + } + catch + { + // just ignore the Exception + } + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs b/backend/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs new file mode 100644 index 000000000..8ec96d9b6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class AsyncLockPool + { + private readonly AsyncLock[] locks; + + public AsyncLockPool(int poolSize) + { + Guard.GreaterThan(poolSize, 0); + + locks = new AsyncLock[poolSize]; + + for (var i = 0; i < poolSize; i++) + { + locks[i] = new AsyncLock(); + } + } + + public Task LockAsync(object target) + { + Guard.NotNull(target); + + return locks[Math.Abs(target.GetHashCode() % locks.Length)].LockAsync(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs b/backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs new file mode 100644 index 000000000..7b89f8e0d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs @@ -0,0 +1,98 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Infrastructure.Tasks +{ + public class PartitionedActionBlock : ITargetBlock + { + private readonly ITargetBlock distributor; + private readonly ActionBlock[] workers; + + public Task Completion + { + get { return Task.WhenAll(workers.Select(x => x.Completion)); } + } + + public PartitionedActionBlock(Action action, Func partitioner) + : this (action?.ToAsync()!, partitioner, new ExecutionDataflowBlockOptions()) + { + } + + public PartitionedActionBlock(Func action, Func partitioner) + : this(action, partitioner, new ExecutionDataflowBlockOptions()) + { + } + + public PartitionedActionBlock(Action action, Func partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) + : this(action?.ToAsync()!, partitioner, dataflowBlockOptions) + { + } + + public PartitionedActionBlock(Func action, Func partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) + { + Guard.NotNull(action); + Guard.NotNull(partitioner); + Guard.NotNull(dataflowBlockOptions); + Guard.GreaterThan(dataflowBlockOptions.MaxDegreeOfParallelism, 1); + + workers = new ActionBlock[dataflowBlockOptions.MaxDegreeOfParallelism]; + + for (var i = 0; i < dataflowBlockOptions.MaxDegreeOfParallelism; i++) + { + var workerOption = SimpleMapper.Map(dataflowBlockOptions, new ExecutionDataflowBlockOptions()); + + workerOption.MaxDegreeOfParallelism = 1; + workerOption.MaxMessagesPerTask = 1; + + workers[i] = new ActionBlock(action, workerOption); + } + + var distributorOption = new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = 1, + MaxMessagesPerTask = 1, + BoundedCapacity = 1 + }; + + distributor = new ActionBlock(x => + { + var partition = Math.Abs(partitioner(x)) % workers.Length; + + return workers[partition].SendAsync(x); + }, distributorOption); + + distributor.Completion.ContinueWith(x => + { + foreach (var worker in workers) + { + worker.Complete(); + } + }); + } + + public DataflowMessageStatus OfferMessage(DataflowMessageHeader messageHeader, TInput messageValue, ISourceBlock source, bool consumeToAccept) + { + return distributor.OfferMessage(messageHeader, messageValue, source, consumeToAccept); + } + + public void Complete() + { + distributor.Complete(); + } + + public void Fault(Exception exception) + { + distributor.Fault(exception); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs b/backend/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs new file mode 100644 index 000000000..244f2c96e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class SingleThreadedDispatcher + { + private readonly ActionBlock> block; + private bool isStopped; + + public SingleThreadedDispatcher(int capacity = 1) + { + var options = new ExecutionDataflowBlockOptions + { + BoundedCapacity = capacity, + MaxMessagesPerTask = 1, + MaxDegreeOfParallelism = 1 + }; + + block = new ActionBlock>(Handle, options); + } + + public Task DispatchAsync(Func action) + { + Guard.NotNull(action); + + return block.SendAsync(action); + } + + public Task DispatchAsync(Action action) + { + Guard.NotNull(action); + + return block.SendAsync(() => { action(); return TaskHelper.Done; }); + } + + public async Task StopAndWaitAsync() + { + await DispatchAsync(() => + { + isStopped = true; + + block.Complete(); + }); + + await block.Completion; + } + + private Task Handle(Func action) + { + if (isStopped) + { + return TaskHelper.Done; + } + + return action(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs b/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs new file mode 100644 index 000000000..4078d8e93 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs @@ -0,0 +1,103 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Tasks +{ + public static class TaskExtensions + { + private static readonly Action IgnoreTaskContinuation = t => { var ignored = t.Exception; }; + + public static void Forget(this Task task) + { + if (task.IsCompleted) + { +#pragma warning disable IDE0059 // Unnecessary assignment of a value + var ignored = task.Exception; +#pragma warning restore IDE0059 // Unnecessary assignment of a value + } + else + { + task.ContinueWith( + IgnoreTaskContinuation, + CancellationToken.None, + TaskContinuationOptions.OnlyOnFaulted | + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + } + + public static Func ToDefault(this Action action) + { + Guard.NotNull(action); + + return x => + { + action(x); + + return default!; + }; + } + + public static Func> ToDefault(this Func action) + { + Guard.NotNull(action); + + return async x => + { + await action(x); + + return default!; + }; + } + + public static Func> ToAsync(this Func action) + { + Guard.NotNull(action); + + return x => + { + var result = action(x); + + return Task.FromResult(result); + }; + } + + public static Func ToAsync(this Action action) + { + return x => + { + action(x); + + return TaskHelper.Done; + }; + } + + public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using (cancellationToken.Register(state => + { + ((TaskCompletionSource)state!).TrySetResult(null!); + }, + tcs)) + { + var resultTask = await Task.WhenAny(task, tcs.Task); + if (resultTask == tcs.Task) + { + throw new OperationCanceledException(cancellationToken); + } + + return await task; + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/TaskHelper.cs b/backend/src/Squidex.Infrastructure/Tasks/TaskHelper.cs new file mode 100644 index 000000000..6e3ca6f64 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/TaskHelper.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Tasks +{ + public static class TaskHelper + { + public static readonly Task Done = CreateDoneTask(); + public static readonly Task False = CreateResultTask(false); + public static readonly Task True = CreateResultTask(true); + + private static Task CreateDoneTask() + { + var result = new TaskCompletionSource(); + + result.SetResult(null); + + return result.Task; + } + + private static Task CreateResultTask(bool value) + { + var result = new TaskCompletionSource(); + + result.SetResult(value); + + return result.Task; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs b/backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs new file mode 100644 index 000000000..ee865d5a2 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs @@ -0,0 +1,89 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Timers +{ + public sealed class CompletionTimer + { + private const int OneCallNotExecuted = 0; + private const int OneCallExecuted = 1; + private const int OneCallRequested = 2; + private readonly CancellationTokenSource stopToken = new CancellationTokenSource(); + private readonly Task runTask; + private int oneCallState; + private CancellationTokenSource? wakeupToken; + + public CompletionTimer(int delayInMs, Func callback, int initialDelay = 0) + { + Guard.NotNull(callback); + Guard.GreaterThan(delayInMs, 0); + + runTask = RunInternalAsync(delayInMs, initialDelay, callback); + } + + public Task StopAsync() + { + stopToken.Cancel(); + + return runTask; + } + + public void SkipCurrentDelay() + { + if (!stopToken.IsCancellationRequested) + { + Interlocked.CompareExchange(ref oneCallState, OneCallRequested, OneCallNotExecuted); + + wakeupToken?.Cancel(); + } + } + + private async Task RunInternalAsync(int delay, int initialDelay, Func callback) + { + try + { + if (initialDelay > 0) + { + await WaitAsync(initialDelay).ConfigureAwait(false); + } + + while (oneCallState == OneCallRequested || !stopToken.IsCancellationRequested) + { + await callback(stopToken.Token).ConfigureAwait(false); + + oneCallState = OneCallExecuted; + + await WaitAsync(delay).ConfigureAwait(false); + } + } + catch + { + return; + } + } + + private async Task WaitAsync(int intervall) + { + try + { + wakeupToken = new CancellationTokenSource(); + + using (var cts = CancellationTokenSource.CreateLinkedTokenSource(stopToken.Token, wakeupToken.Token)) + { + await Task.Delay(intervall, cts.Token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs b/backend/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs new file mode 100644 index 000000000..6c7af39f7 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs @@ -0,0 +1,97 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Squidex.Infrastructure.Json; + +#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + +namespace Squidex.Infrastructure.Translations +{ + public sealed class DeepLTranslator : ITranslator + { + private const string Url = "https://api.deepl.com/v2/translate"; + private readonly HttpClient httpClient = new HttpClient(); + private readonly DeepLTranslatorOptions deepLOptions; + private readonly IJsonSerializer jsonSerializer; + + private sealed class Response + { + public ResponseTranslation[] Translations { get; set; } + } + + private sealed class ResponseTranslation + { + public string Text { get; set; } + } + + public DeepLTranslator(IOptions deepLOptions, IJsonSerializer jsonSerializer) + { + Guard.NotNull(deepLOptions); + Guard.NotNull(jsonSerializer); + + this.deepLOptions = deepLOptions.Value; + + this.jsonSerializer = jsonSerializer; + } + + public async Task Translate(string sourceText, Language targetLanguage, Language? sourceLanguage = null, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(sourceText) || targetLanguage == null) + { + return new Translation(TranslationResult.NotTranslated, sourceText); + } + + if (string.IsNullOrWhiteSpace(deepLOptions.AuthKey)) + { + return new Translation(TranslationResult.NotImplemented); + } + + var parameters = new Dictionary + { + ["auth_key"] = deepLOptions.AuthKey, + ["text"] = sourceText, + ["target_lang"] = GetLanguageCode(targetLanguage) + }; + + if (sourceLanguage != null) + { + parameters["source_lang"] = GetLanguageCode(sourceLanguage); + } + + var response = await httpClient.PostAsync(Url, new FormUrlEncodedContent(parameters), ct); + var responseString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + var result = jsonSerializer.Deserialize(responseString); + + if (result?.Translations?.Length == 1) + { + return new Translation(TranslationResult.Translated, result.Translations[0].Text); + } + } + + if (response.StatusCode == HttpStatusCode.BadRequest) + { + return new Translation(TranslationResult.LanguageNotSupported, resultText: responseString); + } + + return new Translation(TranslationResult.Failed, resultText: responseString); + } + + private static string GetLanguageCode(Language language) + { + return language.Iso2Code.Substring(0, 2).ToUpperInvariant(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs b/backend/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs new file mode 100644 index 000000000..653e3062d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Translations +{ + public sealed class DeepLTranslatorOptions + { + public string? AuthKey { get; set; } + } +} diff --git a/backend/src/Squidex.Infrastructure/Translations/ITranslator.cs b/backend/src/Squidex.Infrastructure/Translations/ITranslator.cs new file mode 100644 index 000000000..84f6df5df --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Translations/ITranslator.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Translations +{ + public interface ITranslator + { + Task Translate(string sourceText, Language targetLanguage, Language? sourceLanguage = null, CancellationToken ct = default); + } +} diff --git a/backend/src/Squidex.Infrastructure/Translations/NoopTranslator.cs b/backend/src/Squidex.Infrastructure/Translations/NoopTranslator.cs new file mode 100644 index 000000000..567d167fb --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Translations/NoopTranslator.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Translations +{ + public sealed class NoopTranslator : ITranslator + { + public Task Translate(string sourceText, Language targetLanguage, Language? sourceLanguage = null, CancellationToken ct = default) + { + var result = new Translation(TranslationResult.NotImplemented); + + return Task.FromResult(result); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Translations/Translation.cs b/backend/src/Squidex.Infrastructure/Translations/Translation.cs new file mode 100644 index 000000000..619259006 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Translations/Translation.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Translations +{ + public sealed class Translation + { + public TranslationResult Result { get; } + + public string? Text { get; } + + public string? ResultText { get; set; } + + public Translation(TranslationResult result, string? text = null, string? resultText = null) + { + Text = text; + Result = result; + ResultText = resultText; + } + } +} diff --git a/src/Squidex.Infrastructure/Translations/TranslationResult.cs b/backend/src/Squidex.Infrastructure/Translations/TranslationResult.cs similarity index 100% rename from src/Squidex.Infrastructure/Translations/TranslationResult.cs rename to backend/src/Squidex.Infrastructure/Translations/TranslationResult.cs diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs new file mode 100644 index 000000000..fd10160fe --- /dev/null +++ b/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs @@ -0,0 +1,198 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.Timers; + +namespace Squidex.Infrastructure.UsageTracking +{ + public sealed class BackgroundUsageTracker : DisposableObjectBase, IUsageTracker + { + public const string CounterTotalCalls = "TotalCalls"; + public const string CounterTotalElapsedMs = "TotalElapsedMs"; + + private const string FallbackCategory = "*"; + private const int Intervall = 60 * 1000; + private readonly IUsageRepository usageRepository; + private readonly ISemanticLog log; + private readonly CompletionTimer timer; + private ConcurrentDictionary<(string Key, string Category), Usage> usages = new ConcurrentDictionary<(string Key, string Category), Usage>(); + + public BackgroundUsageTracker(IUsageRepository usageRepository, ISemanticLog log) + { + Guard.NotNull(usageRepository); + Guard.NotNull(log); + + this.usageRepository = usageRepository; + + this.log = log; + + timer = new CompletionTimer(Intervall, ct => TrackAsync(), Intervall); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + timer.StopAsync().Wait(); + } + } + + public void Next() + { + ThrowIfDisposed(); + + timer.SkipCurrentDelay(); + } + + private async Task TrackAsync() + { + try + { + var today = DateTime.Today; + + var localUsages = Interlocked.Exchange(ref usages, new ConcurrentDictionary<(string Key, string Category), Usage>()); + + if (localUsages.Count > 0) + { + var updates = new UsageUpdate[localUsages.Count]; + var updateIndex = 0; + + foreach (var kvp in localUsages) + { + var counters = new Counters + { + [CounterTotalCalls] = kvp.Value.Count, + [CounterTotalElapsedMs] = kvp.Value.ElapsedMs + }; + + updates[updateIndex].Key = kvp.Key.Key; + updates[updateIndex].Category = kvp.Key.Category; + updates[updateIndex].Counters = counters; + updates[updateIndex].Date = today; + + updateIndex++; + } + + await usageRepository.TrackUsagesAsync(updates); + } + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "TrackUsage") + .WriteProperty("status", "Failed")); + } + } + + public Task TrackAsync(string key, string? category, double weight, double elapsedMs) + { + key = GetKey(key); + + ThrowIfDisposed(); + + if (weight > 0) + { + category = GetCategory(category); + + usages.AddOrUpdate((key, category), _ => new Usage(elapsedMs, weight), (k, x) => x.Add(elapsedMs, weight)); + } + + return TaskHelper.Done; + } + + public async Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate) + { + key = GetKey(key); + + ThrowIfDisposed(); + + var usagesFlat = await usageRepository.QueryAsync(key, fromDate, toDate); + var usagesByCategory = usagesFlat.GroupBy(x => GetCategory(x.Category)).ToDictionary(x => x.Key, x => x.ToList()); + + var result = new Dictionary>(); + + IEnumerable categories = usagesByCategory.Keys; + + if (usagesByCategory.Count == 0) + { + var enriched = new List(); + + for (var date = fromDate; date <= toDate; date = date.AddDays(1)) + { + enriched.Add(new DateUsage(date, 0, 0)); + } + + result[FallbackCategory] = enriched; + } + else + { + foreach (var category in categories) + { + var enriched = new List(); + + var usagesDictionary = usagesByCategory[category].ToDictionary(x => x.Date); + + for (var date = fromDate; date <= toDate; date = date.AddDays(1)) + { + var stored = usagesDictionary.GetOrDefault(date); + + var totalCount = 0L; + var totalElapsedMs = 0L; + + if (stored != null) + { + totalCount = (long)stored.Counters.Get(CounterTotalCalls); + totalElapsedMs = (long)stored.Counters.Get(CounterTotalElapsedMs); + } + + enriched.Add(new DateUsage(date, totalCount, totalElapsedMs)); + } + + result[category] = enriched; + } + } + + return result; + } + + public Task GetMonthlyCallsAsync(string key, DateTime date) + { + return GetPreviousCallsAsync(key, new DateTime(date.Year, date.Month, 1), date); + } + + public async Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate) + { + key = GetKey(key); + + ThrowIfDisposed(); + + var originalUsages = await usageRepository.QueryAsync(key, fromDate, toDate); + + return originalUsages.Sum(x => (long)x.Counters.Get(CounterTotalCalls)); + } + + private static string GetCategory(string? category) + { + return !string.IsNullOrWhiteSpace(category) ? category.Trim() : FallbackCategory; + } + + private static string GetKey(string key) + { + Guard.NotNull(key); + + return $"{key}_API"; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs new file mode 100644 index 000000000..5d17385e4 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs @@ -0,0 +1,71 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using Squidex.Infrastructure.Caching; + +namespace Squidex.Infrastructure.UsageTracking +{ + public sealed class CachingUsageTracker : CachingProviderBase, IUsageTracker + { + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); + private readonly IUsageTracker inner; + + public CachingUsageTracker(IUsageTracker inner, IMemoryCache cache) + : base(cache) + { + Guard.NotNull(inner); + + this.inner = inner; + } + + public Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate) + { + Guard.NotNull(key); + + return inner.QueryAsync(key, fromDate, toDate); + } + + public Task TrackAsync(string key, string? category, double weight, double elapsedMs) + { + Guard.NotNull(key); + + return inner.TrackAsync(key, category, weight, elapsedMs); + } + + public Task GetMonthlyCallsAsync(string key, DateTime date) + { + Guard.NotNull(key); + + var cacheKey = string.Join("$", "Usage", nameof(GetMonthlyCallsAsync), key, date); + + return Cache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheDuration; + + return inner.GetMonthlyCallsAsync(key, date); + }); + } + + public Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate) + { + Guard.NotNull(key); + + var cacheKey = string.Join("$", "Usage", nameof(GetPreviousCallsAsync), key, fromDate, toDate); + + return Cache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheDuration; + + return inner.GetPreviousCallsAsync(key, fromDate, toDate); + }); + } + } +} diff --git a/src/Squidex.Infrastructure/UsageTracking/Counters.cs b/backend/src/Squidex.Infrastructure/UsageTracking/Counters.cs similarity index 100% rename from src/Squidex.Infrastructure/UsageTracking/Counters.cs rename to backend/src/Squidex.Infrastructure/UsageTracking/Counters.cs diff --git a/src/Squidex.Infrastructure/UsageTracking/DateUsage.cs b/backend/src/Squidex.Infrastructure/UsageTracking/DateUsage.cs similarity index 100% rename from src/Squidex.Infrastructure/UsageTracking/DateUsage.cs rename to backend/src/Squidex.Infrastructure/UsageTracking/DateUsage.cs diff --git a/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs b/backend/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs similarity index 100% rename from src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs rename to backend/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs new file mode 100644 index 000000000..772e82ba1 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.UsageTracking +{ + public interface IUsageTracker + { + Task TrackAsync(string key, string? category, double weight, double elapsedMs); + + Task GetMonthlyCallsAsync(string key, DateTime date); + + Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate); + + Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate); + } +} diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs b/backend/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs new file mode 100644 index 000000000..682c48577 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.UsageTracking +{ + public sealed class StoredUsage + { + public string? Category { get; } + + public DateTime Date { get; } + + public Counters Counters { get; } + + public StoredUsage(string? category, DateTime date, Counters counters) + { + Guard.NotNull(counters); + + Category = category; + Counters = counters; + + Date = date; + } + } +} diff --git a/src/Squidex.Infrastructure/UsageTracking/Usage.cs b/backend/src/Squidex.Infrastructure/UsageTracking/Usage.cs similarity index 100% rename from src/Squidex.Infrastructure/UsageTracking/Usage.cs rename to backend/src/Squidex.Infrastructure/UsageTracking/Usage.cs diff --git a/src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs b/backend/src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs similarity index 100% rename from src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs rename to backend/src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs diff --git a/src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs similarity index 100% rename from src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs rename to backend/src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs diff --git a/src/Squidex.Infrastructure/Validation/IValidatable.cs b/backend/src/Squidex.Infrastructure/Validation/IValidatable.cs similarity index 100% rename from src/Squidex.Infrastructure/Validation/IValidatable.cs rename to backend/src/Squidex.Infrastructure/Validation/IValidatable.cs diff --git a/src/Squidex.Infrastructure/Validation/Not.cs b/backend/src/Squidex.Infrastructure/Validation/Not.cs similarity index 100% rename from src/Squidex.Infrastructure/Validation/Not.cs rename to backend/src/Squidex.Infrastructure/Validation/Not.cs diff --git a/backend/src/Squidex.Infrastructure/Validation/Validate.cs b/backend/src/Squidex.Infrastructure/Validation/Validate.cs new file mode 100644 index 000000000..95e3a8db5 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Validation/Validate.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Validation +{ + public static class Validate + { + public static void It(Func message, Action action) + { + List? errors = null; + + var addValidation = new AddValidation((m, p) => + { + if (errors == null) + { + errors = new List(); + } + + errors.Add(new ValidationError(m, p)); + }); + + action(addValidation); + + if (errors != null) + { + throw new ValidationException(message(), errors); + } + } + + public static async Task It(Func message, Func action) + { + List? errors = null; + + var addValidation = new AddValidation((m, p) => + { + if (errors == null) + { + errors = new List(); + } + + errors.Add(new ValidationError(m, p)); + }); + + await action(addValidation); + + if (errors != null) + { + throw new ValidationException(message(), errors); + } + } + } + + public delegate void AddValidation(string message, params string[] propertyNames); +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Validation/ValidationError.cs b/backend/src/Squidex.Infrastructure/Validation/ValidationError.cs new file mode 100644 index 000000000..7de0d81ef --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Validation/ValidationError.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Squidex.Infrastructure.Validation +{ + [Serializable] + public sealed class ValidationError + { + private readonly string message; + private readonly string[] propertyNames; + + public string Message + { + get { return message; } + } + + public IEnumerable PropertyNames + { + get { return propertyNames; } + } + + public ValidationError(string message, params string[] propertyNames) + { + Guard.NotNullOrEmpty(message); + + this.message = message; + + this.propertyNames = propertyNames ?? Array.Empty(); + } + + public ValidationError WithPrefix(string prefix) + { + if (propertyNames.Length > 0) + { + return new ValidationError(Message, propertyNames.Select(x => $"{prefix}.{x}").ToArray()); + } + else + { + return new ValidationError(Message, prefix); + } + } + + public void AddTo(AddValidation e) + { + e(Message, propertyNames); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Validation/ValidationException.cs b/backend/src/Squidex.Infrastructure/Validation/ValidationException.cs new file mode 100644 index 000000000..fb3a4bd10 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Validation/ValidationException.cs @@ -0,0 +1,106 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; + +namespace Squidex.Infrastructure.Validation +{ + [Serializable] + public class ValidationException : DomainException + { + private static readonly List FallbackErrors = new List(); + private readonly IReadOnlyList errors; + + public IReadOnlyList Errors + { + get { return errors ?? FallbackErrors; } + } + + public string Summary { get; } + + public ValidationException(string summary, params ValidationError[]? errors) + : this(summary, errors?.ToList()) + { + } + + public ValidationException(string summary, IReadOnlyList? errors) + : this(summary, null, errors) + { + } + + public ValidationException(string summary, Exception? inner, params ValidationError[]? errors) + : this(summary, inner, errors?.ToList()) + { + } + + public ValidationException(string summary, Exception? inner, IReadOnlyList? errors) + : base(FormatMessage(summary, errors), inner!) + { + Summary = summary; + + this.errors = errors ?? FallbackErrors; + } + + protected ValidationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Summary = info.GetString(nameof(Summary))!; + + errors = (List)info.GetValue(nameof(errors), typeof(List))!; + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(Summary), Summary); + info.AddValue(nameof(errors), errors.ToList()); + + base.GetObjectData(info, context); + } + + private static string FormatMessage(string summary, IReadOnlyList? errors) + { + var sb = new StringBuilder(); + + sb.Append(summary.TrimEnd(' ', '.', ':')); + + if (errors?.Count > 0) + { + sb.Append(": "); + + for (var i = 0; i < errors.Count; i++) + { + var error = errors[i]?.Message; + + if (!string.IsNullOrWhiteSpace(error)) + { + sb.Append(error); + + if (!error.EndsWith(".", StringComparison.OrdinalIgnoreCase)) + { + sb.Append("."); + } + + if (i < errors.Count - 1) + { + sb.Append(" "); + } + } + } + } + else + { + sb.Append("."); + } + + return sb.ToString(); + } + } +} diff --git a/src/Squidex.Infrastructure/Validation/ValidationExtensions.cs b/backend/src/Squidex.Infrastructure/Validation/ValidationExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/Validation/ValidationExtensions.cs rename to backend/src/Squidex.Infrastructure/Validation/ValidationExtensions.cs diff --git a/src/Squidex.Infrastructure/ValueStopwatch.cs b/backend/src/Squidex.Infrastructure/ValueStopwatch.cs similarity index 100% rename from src/Squidex.Infrastructure/ValueStopwatch.cs rename to backend/src/Squidex.Infrastructure/ValueStopwatch.cs diff --git a/src/Squidex.Infrastructure/language-codes.csv b/backend/src/Squidex.Infrastructure/language-codes.csv similarity index 100% rename from src/Squidex.Infrastructure/language-codes.csv rename to backend/src/Squidex.Infrastructure/language-codes.csv diff --git a/src/Squidex.Shared/DefaultClients.cs b/backend/src/Squidex.Shared/DefaultClients.cs similarity index 100% rename from src/Squidex.Shared/DefaultClients.cs rename to backend/src/Squidex.Shared/DefaultClients.cs diff --git a/src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs b/backend/src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs similarity index 100% rename from src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs rename to backend/src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs diff --git a/src/Squidex.Shared/Identity/SquidexClaimTypes.cs b/backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs similarity index 100% rename from src/Squidex.Shared/Identity/SquidexClaimTypes.cs rename to backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs diff --git a/backend/src/Squidex.Shared/Permissions.cs b/backend/src/Squidex.Shared/Permissions.cs new file mode 100644 index 000000000..b146e89db --- /dev/null +++ b/backend/src/Squidex.Shared/Permissions.cs @@ -0,0 +1,184 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; + +namespace Squidex.Shared +{ + public static class Permissions + { + private static readonly List ForAppsNonSchemaList = new List(); + private static readonly List ForAppsSchemaList = new List(); + + public static IReadOnlyList ForAppsNonSchema + { + get { return ForAppsNonSchemaList; } + } + + public static IReadOnlyList ForAppsSchema + { + get { return ForAppsSchemaList; } + } + + public const string All = "squidex.*"; + + public const string Admin = "squidex.admin.*"; + public const string AdminOrleans = "squidex.admin.orleans"; + + public const string AdminAppCreate = "squidex.admin.apps.create"; + + public const string AdminRestore = "squidex.admin.restore"; + + public const string AdminEvents = "squidex.admin.events"; + public const string AdminEventsRead = "squidex.admin.events.read"; + public const string AdminEventsManage = "squidex.admin.events.manage"; + + public const string AdminUsers = "squidex.admin.users"; + public const string AdminUsersRead = "squidex.admin.users.read"; + public const string AdminUsersCreate = "squidex.admin.users.create"; + public const string AdminUsersUpdate = "squidex.admin.users.update"; + public const string AdminUsersUnlock = "squidex.admin.users.unlock"; + public const string AdminUsersLock = "squidex.admin.users.lock"; + + public const string App = "squidex.apps.{app}"; + public const string AppCommon = "squidex.apps.{app}.common"; + + public const string AppDelete = "squidex.apps.{app}.delete"; + public const string AppUpdate = "squidex.apps.{app}.update"; + public const string AppUpdateImage = "squidex.apps.{app}.update"; + public const string AppUpdateGeneral = "squidex.apps.{app}.general"; + + public const string AppClients = "squidex.apps.{app}.clients"; + public const string AppClientsRead = "squidex.apps.{app}.clients.read"; + public const string AppClientsCreate = "squidex.apps.{app}.clients.create"; + public const string AppClientsUpdate = "squidex.apps.{app}.clients.update"; + public const string AppClientsDelete = "squidex.apps.{app}.clients.delete"; + + public const string AppContributors = "squidex.apps.{app}.contributors"; + public const string AppContributorsRead = "squidex.apps.{app}.contributors.read"; + public const string AppContributorsAssign = "squidex.apps.{app}.contributors.assign"; + public const string AppContributorsRevoke = "squidex.apps.{app}.contributors.revoke"; + + public const string AppLanguages = "squidex.apps.{app}.languages"; + public const string AppLanguagesCreate = "squidex.apps.{app}.languages.create"; + public const string AppLanguagesUpdate = "squidex.apps.{app}.languages.update"; + public const string AppLanguagesDelete = "squidex.apps.{app}.languages.delete"; + + public const string AppRoles = "squidex.apps.{app}.roles"; + public const string AppRolesRead = "squidex.apps.{app}.roles.read"; + public const string AppRolesCreate = "squidex.apps.{app}.roles.create"; + public const string AppRolesUpdate = "squidex.apps.{app}.roles.update"; + public const string AppRolesDelete = "squidex.apps.{app}.roles.delete"; + + public const string AppPatterns = "squidex.apps.{app}.patterns"; + public const string AppPatternsCreate = "squidex.apps.{app}.patterns.create"; + public const string AppPatternsUpdate = "squidex.apps.{app}.patterns.update"; + public const string AppPatternsDelete = "squidex.apps.{app}.patterns.delete"; + + public const string AppWorkflows = "squidex.apps.{app}.workflows"; + public const string AppWorkflowsRead = "squidex.apps.{app}.workflows.read"; + public const string AppWorkflowsCreate = "squidex.apps.{app}.workflows.create"; + public const string AppWorkflowsUpdate = "squidex.apps.{app}.workflows.update"; + public const string AppWorkflowsDelete = "squidex.apps.{app}.workflows.delete"; + + public const string AppBackups = "squidex.apps.{app}.backups"; + public const string AppBackupsRead = "squidex.apps.{app}.backups.read"; + public const string AppBackupsCreate = "squidex.apps.{app}.backups.create"; + public const string AppBackupsDelete = "squidex.apps.{app}.backups.delete"; + + public const string AppPlans = "squidex.apps.{app}.plans"; + public const string AppPlansRead = "squidex.apps.{app}.plans.read"; + public const string AppPlansChange = "squidex.apps.{app}.plans.change"; + + public const string AppAssets = "squidex.apps.{app}.assets"; + public const string AppAssetsRead = "squidex.apps.{app}.assets.read"; + public const string AppAssetsCreate = "squidex.apps.{app}.assets.create"; + public const string AppAssetsUpdate = "squidex.apps.{app}.assets.update"; + public const string AppAssetsDelete = "squidex.apps.{app}.assets.delete"; + + public const string AppRules = "squidex.apps.{app}.rules"; + public const string AppRulesRead = "squidex.apps.{app}.rules.read"; + public const string AppRulesEvents = "squidex.apps.{app}.rules.events"; + public const string AppRulesCreate = "squidex.apps.{app}.rules.create"; + public const string AppRulesUpdate = "squidex.apps.{app}.rules.update"; + public const string AppRulesDisable = "squidex.apps.{app}.rules.disable"; + public const string AppRulesDelete = "squidex.apps.{app}.rules.delete"; + + public const string AppSchemas = "squidex.apps.{app}.schemas.{name}"; + public const string AppSchemasCreate = "squidex.apps.{app}.schemas.{name}.create"; + public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{name}.update"; + public const string AppSchemasScripts = "squidex.apps.{app}.schemas.{name}.scripts"; + public const string AppSchemasPublish = "squidex.apps.{app}.schemas.{name}.publish"; + public const string AppSchemasDelete = "squidex.apps.{app}.schemas.{name}.delete"; + + public const string AppContents = "squidex.apps.{app}.contents.{name}"; + public const string AppContentsRead = "squidex.apps.{app}.contents.{name}.read"; + public const string AppContentsCreate = "squidex.apps.{app}.contents.{name}.create"; + public const string AppContentsUpdate = "squidex.apps.{app}.contents.{name}.update"; + public const string AppContentsDraftDiscard = "squidex.apps.{app}.contents.{name}.draft.discard"; + public const string AppContentsDraftPublish = "squidex.apps.{app}.contents.{name}.draft.publish"; + public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete"; + + public const string AppApi = "squidex.apps.{app}.api"; + + static Permissions() + { + foreach (var field in typeof(Permissions).GetFields(BindingFlags.Public | BindingFlags.Static)) + { + if (field.IsLiteral && !field.IsInitOnly) + { + var value = field.GetValue(null) as string; + + if (value?.StartsWith(App, StringComparison.OrdinalIgnoreCase) == true) + { + if (value.IndexOf("{name}", App.Length, StringComparison.OrdinalIgnoreCase) >= 0) + { + ForAppsSchemaList.Add(value); + } + else + { + ForAppsNonSchemaList.Add(value); + } + } + } + } + } + + public static Permission ForApp(string id, string app = Permission.Any, string schema = Permission.Any) + { + Guard.NotNull(id); + + return new Permission(id.Replace("{app}", app ?? Permission.Any).Replace("{name}", schema ?? Permission.Any)); + } + + public static PermissionSet ToAppPermissions(this PermissionSet permissions, string app) + { + var matching = permissions.Where(x => x.StartsWith($"squidex.apps.{app}")); + + return new PermissionSet(matching); + } + + public static string[] ToAppNames(this PermissionSet permissions) + { + var matching = permissions.Where(x => x.StartsWith("squidex.apps.")); + + var result = + matching + .Select(x => x.Id.Split('.')).Where(x => x.Length > 2) + .Select(x => x[2]) + .Distinct() + .ToArray(); + + return result; + } + } +} diff --git a/backend/src/Squidex.Shared/Squidex.Shared.csproj b/backend/src/Squidex.Shared/Squidex.Shared.csproj new file mode 100644 index 000000000..57f3b5d1f --- /dev/null +++ b/backend/src/Squidex.Shared/Squidex.Shared.csproj @@ -0,0 +1,25 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + ..\..\Squidex.ruleset + + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex.Shared/Users/ClientUser.cs b/backend/src/Squidex.Shared/Users/ClientUser.cs new file mode 100644 index 000000000..5958dc5de --- /dev/null +++ b/backend/src/Squidex.Shared/Users/ClientUser.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Security.Claims; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Squidex.Shared.Identity; + +namespace Squidex.Shared.Users +{ + public sealed class ClientUser : IUser + { + private readonly RefToken token; + private readonly List claims; + + public ClientUser(RefToken token) + { + Guard.NotNull(token); + + this.token = token; + + claims = new List + { + new Claim(OpenIdClaims.ClientId, token.Identifier), + new Claim(SquidexClaimTypes.DisplayName, token.ToString()) + }; + } + + public string Id + { + get { return token.Identifier; } + } + + public string Email + { + get { return token.ToString(); } + } + + public bool IsLocked + { + get { return false; } + } + + public IReadOnlyList Claims + { + get { return claims; } + } + } +} diff --git a/src/Squidex.Shared/Users/IUser.cs b/backend/src/Squidex.Shared/Users/IUser.cs similarity index 100% rename from src/Squidex.Shared/Users/IUser.cs rename to backend/src/Squidex.Shared/Users/IUser.cs diff --git a/backend/src/Squidex.Shared/Users/IUserResolver.cs b/backend/src/Squidex.Shared/Users/IUserResolver.cs new file mode 100644 index 000000000..429930038 --- /dev/null +++ b/backend/src/Squidex.Shared/Users/IUserResolver.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Shared.Users +{ + public interface IUserResolver + { + Task CreateUserIfNotExists(string email, bool invited = false); + + Task FindByIdOrEmailAsync(string idOrEmail); + + Task> QueryByEmailAsync(string email); + + Task> QueryManyAsync(string[] ids); + } +} diff --git a/backend/src/Squidex.Shared/Users/UserExtensions.cs b/backend/src/Squidex.Shared/Users/UserExtensions.cs new file mode 100644 index 000000000..7a2e7c2b5 --- /dev/null +++ b/backend/src/Squidex.Shared/Users/UserExtensions.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using Squidex.Infrastructure.Security; +using Squidex.Shared.Identity; + +namespace Squidex.Shared.Users +{ + public static class UserExtensions + { + public static PermissionSet Permissions(this IUser user) + { + return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x))); + } + + public static bool IsInvited(this IUser user) + { + return user.HasClaimValue(SquidexClaimTypes.Invited, "true"); + } + + public static bool IsHidden(this IUser user) + { + return user.HasClaimValue(SquidexClaimTypes.Hidden, "true"); + } + + public static bool HasConsent(this IUser user) + { + return user.HasClaimValue(SquidexClaimTypes.Consent, "true"); + } + + public static bool HasConsentForEmails(this IUser user) + { + return user.HasClaimValue(SquidexClaimTypes.ConsentForEmails, "true"); + } + + public static bool HasDisplayName(this IUser user) + { + return user.HasClaim(SquidexClaimTypes.DisplayName); + } + + public static bool HasPictureUrl(this IUser user) + { + return user.HasClaim(SquidexClaimTypes.PictureUrl); + } + + public static bool IsPictureUrlStored(this IUser user) + { + return user.HasClaimValue(SquidexClaimTypes.PictureUrl, SquidexClaimTypes.PictureUrlStore); + } + + public static string? ClientSecret(this IUser user) + { + return user.GetClaimValue(SquidexClaimTypes.ClientSecret); + } + + public static string? PictureUrl(this IUser user) + { + return user.GetClaimValue(SquidexClaimTypes.PictureUrl); + } + + public static string? DisplayName(this IUser user) + { + return user.GetClaimValue(SquidexClaimTypes.DisplayName); + } + + public static string? GetClaimValue(this IUser user, string type) + { + return user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase))?.Value; + } + + public static string[] GetClaimValues(this IUser user, string type) + { + return user.Claims.Where(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)).Select(x => x.Value).ToArray(); + } + + public static bool HasClaim(this IUser user, string type) + { + return user.Claims.Any(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); + } + + public static bool HasClaimValue(this IUser user, string type, string value) + { + return user.Claims.Any(x => + string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase) && + string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase)); + } + + public static string? PictureNormalizedUrl(this IUser user) + { + var url = user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.PictureUrl)?.Value; + + if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar")) + { + if (url.Contains("?")) + { + url += "&d=404"; + } + else + { + url += "?d=404"; + } + } + + return url; + } + } +} diff --git a/backend/src/Squidex.Web/ApiController.cs b/backend/src/Squidex.Web/ApiController.cs new file mode 100644 index 000000000..035512bfe --- /dev/null +++ b/backend/src/Squidex.Web/ApiController.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Web +{ + [Area("Api")] + [ApiController] + [ApiExceptionFilter] + [ApiModelValidation(false)] + public abstract class ApiController : Controller + { + protected ICommandBus CommandBus { get; } + + protected IAppEntity App + { + get + { + var app = HttpContext.Context().App; + + if (app == null) + { + throw new InvalidOperationException("Not in a app context."); + } + + return app; + } + } + + protected Context Context + { + get { return HttpContext.Context(); } + } + + protected Guid AppId + { + get { return App.Id; } + } + + protected ApiController(ICommandBus commandBus) + { + Guard.NotNull(commandBus); + + CommandBus = commandBus; + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + var request = context.HttpContext.Request; + + if (!request.PathBase.HasValue || !request.PathBase.Value.EndsWith("/api", StringComparison.OrdinalIgnoreCase)) + { + context.Result = new RedirectResult("/"); + } + } + } +} diff --git a/src/Squidex.Web/ApiCostsAttribute.cs b/backend/src/Squidex.Web/ApiCostsAttribute.cs similarity index 100% rename from src/Squidex.Web/ApiCostsAttribute.cs rename to backend/src/Squidex.Web/ApiCostsAttribute.cs diff --git a/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs b/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs new file mode 100644 index 000000000..125e6169b --- /dev/null +++ b/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs @@ -0,0 +1,117 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security; +using System.Text; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Web +{ + public sealed class ApiExceptionFilterAttribute : ActionFilterAttribute, IExceptionFilter + { + private static readonly List> Handlers = new List>(); + + private static void AddHandler(Func handler) where T : Exception + { + Handlers.Add(ex => ex is T typed ? handler(typed) : null); + } + + static ApiExceptionFilterAttribute() + { + AddHandler(OnValidationException); + AddHandler(OnDecoderException); + AddHandler(OnDomainObjectNotFoundException); + AddHandler(OnDomainObjectVersionException); + AddHandler(OnDomainForbiddenException); + AddHandler(OnDomainException); + AddHandler(OnSecurityException); + } + + private static IActionResult OnDecoderException(DecoderFallbackException ex) + { + return ErrorResult(400, new ErrorDto { Message = ex.Message }); + } + + private static IActionResult OnDomainObjectNotFoundException(DomainObjectNotFoundException ex) + { + return new NotFoundResult(); + } + + private static IActionResult OnDomainObjectVersionException(DomainObjectVersionException ex) + { + return ErrorResult(412, new ErrorDto { Message = ex.Message }); + } + + private static IActionResult OnDomainException(DomainException ex) + { + return ErrorResult(400, new ErrorDto { Message = ex.Message }); + } + + private static IActionResult OnDomainForbiddenException(DomainForbiddenException ex) + { + return ErrorResult(403, new ErrorDto { Message = ex.Message }); + } + + private static IActionResult OnSecurityException(SecurityException ex) + { + return ErrorResult(403, new ErrorDto { Message = ex.Message }); + } + + private static IActionResult OnValidationException(ValidationException ex) + { + return ErrorResult(400, new ErrorDto { Message = ex.Summary, Details = ToDetails(ex) }); + } + + private static IActionResult ErrorResult(int statusCode, ErrorDto error) + { + error.StatusCode = statusCode; + + return new ObjectResult(error) { StatusCode = statusCode }; + } + + public void OnException(ExceptionContext context) + { + IActionResult? result = null; + + foreach (var handler in Handlers) + { + result = handler(context.Exception); + + if (result != null) + { + break; + } + } + + if (result != null) + { + context.Result = result; + } + } + + private static string[] ToDetails(ValidationException ex) + { + return ex.Errors?.Select(e => + { + if (e.PropertyNames?.Any() == true) + { + return $"{string.Join(", ", e.PropertyNames)}: {e.Message}"; + } + else + { + return e.Message; + } + }).ToArray() ?? new string[0]; + } + } +} diff --git a/src/Squidex.Web/ApiModelValidationAttribute.cs b/backend/src/Squidex.Web/ApiModelValidationAttribute.cs similarity index 100% rename from src/Squidex.Web/ApiModelValidationAttribute.cs rename to backend/src/Squidex.Web/ApiModelValidationAttribute.cs diff --git a/src/Squidex.Web/ApiPermissionAttribute.cs b/backend/src/Squidex.Web/ApiPermissionAttribute.cs similarity index 100% rename from src/Squidex.Web/ApiPermissionAttribute.cs rename to backend/src/Squidex.Web/ApiPermissionAttribute.cs diff --git a/backend/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs b/backend/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs new file mode 100644 index 000000000..7a85779d4 --- /dev/null +++ b/backend/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Entities.Assets; + +namespace Squidex.Web +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public sealed class AssetRequestSizeLimitAttribute : Attribute, IAuthorizationFilter, IRequestSizePolicy + { + public void OnAuthorization(AuthorizationFilterContext context) + { + var assetOptions = context.HttpContext.RequestServices.GetService>(); + + var maxRequestBodySizeFeature = context.HttpContext.Features.Get(); + + if (maxRequestBodySizeFeature?.IsReadOnly == false) + { + if (assetOptions?.Value.MaxSize > 0) + { + maxRequestBodySizeFeature.MaxRequestBodySize = assetOptions.Value.MaxSize; + } + else + { + maxRequestBodySizeFeature.MaxRequestBodySize = null; + } + } + } + } +} diff --git a/src/Squidex.Web/ClearCookiesAttribute.cs b/backend/src/Squidex.Web/ClearCookiesAttribute.cs similarity index 100% rename from src/Squidex.Web/ClearCookiesAttribute.cs rename to backend/src/Squidex.Web/ClearCookiesAttribute.cs diff --git a/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs similarity index 100% rename from src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs rename to backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs diff --git a/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs similarity index 100% rename from src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs rename to backend/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs diff --git a/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs similarity index 100% rename from src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs rename to backend/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs diff --git a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs new file mode 100644 index 000000000..03435941b --- /dev/null +++ b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Web.CommandMiddlewares +{ + public sealed class EnrichWithSchemaIdCommandMiddleware : ICommandMiddleware + { + private readonly IAppProvider appProvider; + private readonly IActionContextAccessor actionContextAccessor; + + public EnrichWithSchemaIdCommandMiddleware(IAppProvider appProvider, IActionContextAccessor actionContextAccessor) + { + this.appProvider = appProvider; + + this.actionContextAccessor = actionContextAccessor; + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (actionContextAccessor.ActionContext == null) + { + await next(); + + return; + } + + if (context.Command is ISchemaCommand schemaCommand && schemaCommand.SchemaId == null) + { + var schemaId = await GetSchemaIdAsync(context); + + schemaCommand.SchemaId = schemaId!; + } + + if (context.Command is SchemaCommand schemaSelfCommand && schemaSelfCommand.SchemaId == Guid.Empty) + { + var schemaId = await GetSchemaIdAsync(context); + + schemaSelfCommand.SchemaId = schemaId?.Id ?? Guid.Empty; + } + + await next(); + } + + private async Task?> GetSchemaIdAsync(CommandContext context) + { + NamedId? appId = null; + + if (context.Command is IAppCommand appCommand) + { + appId = appCommand.AppId; + } + + if (appId == null) + { + appId = actionContextAccessor.ActionContext.HttpContext.Context().App?.NamedId(); + } + + if (appId != null) + { + var routeValues = actionContextAccessor.ActionContext.RouteData.Values; + + if (routeValues.ContainsKey("name")) + { + var schemaName = routeValues["name"].ToString()!; + + ISchemaEntity? schema; + + if (Guid.TryParse(schemaName, out var id)) + { + schema = await appProvider.GetSchemaAsync(appId.Id, id); + } + else + { + schema = await appProvider.GetSchemaAsync(appId.Id, schemaName); + } + + if (schema == null) + { + throw new DomainObjectNotFoundException(schemaName, typeof(ISchemaEntity)); + } + + return schema.NamedId(); + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Web/Constants.cs b/backend/src/Squidex.Web/Constants.cs similarity index 100% rename from src/Squidex.Web/Constants.cs rename to backend/src/Squidex.Web/Constants.cs diff --git a/src/Squidex.Web/ContextExtensions.cs b/backend/src/Squidex.Web/ContextExtensions.cs similarity index 100% rename from src/Squidex.Web/ContextExtensions.cs rename to backend/src/Squidex.Web/ContextExtensions.cs diff --git a/backend/src/Squidex.Web/ContextProvider.cs b/backend/src/Squidex.Web/ContextProvider.cs new file mode 100644 index 000000000..e56c8ae26 --- /dev/null +++ b/backend/src/Squidex.Web/ContextProvider.cs @@ -0,0 +1,45 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using Microsoft.AspNetCore.Http; +using Squidex.Domain.Apps.Entities; +using Squidex.Infrastructure; + +namespace Squidex.Web +{ + public sealed class ContextProvider : IContextProvider + { + private readonly IHttpContextAccessor httpContextAccessor; + private readonly AsyncLocal asyncLocal = new AsyncLocal(); + + public Context Context + { + get + { + if (httpContextAccessor.HttpContext == null) + { + if (asyncLocal.Value == null) + { + asyncLocal.Value = Context.Anonymous(); + } + + return asyncLocal.Value; + } + + return httpContextAccessor.HttpContext.Context(); + } + } + + public ContextProvider(IHttpContextAccessor httpContextAccessor) + { + Guard.NotNull(httpContextAccessor); + + this.httpContextAccessor = httpContextAccessor; + } + } +} diff --git a/backend/src/Squidex.Web/Deferred.cs b/backend/src/Squidex.Web/Deferred.cs new file mode 100644 index 000000000..9779689ca --- /dev/null +++ b/backend/src/Squidex.Web/Deferred.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Web +{ + public struct Deferred + { + private readonly Lazy> value; + + public Task Value + { + get { return value.Value; } + } + + private Deferred(Func> value) + { + this.value = new Lazy>(value); + } + + public static Deferred Response(Func factory) + { + Guard.NotNull(factory); + + return new Deferred(() => Task.FromResult(factory())); + } + + public static Deferred AsyncResponse(Func> factory) + { + Guard.NotNull(factory); + + return new Deferred(async () => (await factory())!); + } + } +} diff --git a/src/Squidex.Web/ETagExtensions.cs b/backend/src/Squidex.Web/ETagExtensions.cs similarity index 100% rename from src/Squidex.Web/ETagExtensions.cs rename to backend/src/Squidex.Web/ETagExtensions.cs diff --git a/backend/src/Squidex.Web/EntityCreatedDto.cs b/backend/src/Squidex.Web/EntityCreatedDto.cs new file mode 100644 index 000000000..4946e1af8 --- /dev/null +++ b/backend/src/Squidex.Web/EntityCreatedDto.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Web +{ + public sealed class EntityCreatedDto + { + [Required] + [Display(Description = "Id of the created entity.")] + public string? Id { get; set; } + + [Display(Description = "The new version of the entity.")] + public long Version { get; set; } + + public static EntityCreatedDto FromResult(EntityCreatedResult result) + { + return new EntityCreatedDto { Id = result.IdOrValue?.ToString(), Version = result.Version }; + } + } +} diff --git a/src/Squidex.Web/ErrorDto.cs b/backend/src/Squidex.Web/ErrorDto.cs similarity index 100% rename from src/Squidex.Web/ErrorDto.cs rename to backend/src/Squidex.Web/ErrorDto.cs diff --git a/src/Squidex.Web/ExposedConfiguration.cs b/backend/src/Squidex.Web/ExposedConfiguration.cs similarity index 100% rename from src/Squidex.Web/ExposedConfiguration.cs rename to backend/src/Squidex.Web/ExposedConfiguration.cs diff --git a/backend/src/Squidex.Web/ExposedValues.cs b/backend/src/Squidex.Web/ExposedValues.cs new file mode 100644 index 000000000..d1d3c7bd4 --- /dev/null +++ b/backend/src/Squidex.Web/ExposedValues.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.Reflection; +using System.Text; +using Microsoft.Extensions.Configuration; +using Squidex.Infrastructure; + +namespace Squidex.Web +{ + public sealed class ExposedValues : Dictionary + { + public ExposedValues() + { + } + + public ExposedValues(ExposedConfiguration configured, IConfiguration configuration, Assembly? assembly = null) + { + Guard.NotNull(configured); + Guard.NotNull(configuration); + + foreach (var kvp in configured) + { + var value = configuration.GetValue(kvp.Value); + + if (!string.IsNullOrWhiteSpace(value)) + { + this[kvp.Key] = value; + } + } + + if (assembly != null) + { + if (!ContainsKey("version")) + { + this["version"] = assembly.GetName()!.Version!.ToString(); + } + } + } + + public override string ToString() + { + var sb = new StringBuilder(); + + foreach (var kvp in this) + { + if (sb.Length > 0) + { + sb.Append(", "); + } + + sb.Append(kvp.Key); + sb.Append(": "); + sb.Append(kvp.Value); + } + + return sb.ToString(); + } + } +} diff --git a/backend/src/Squidex.Web/Extensions.cs b/backend/src/Squidex.Web/Extensions.cs new file mode 100644 index 000000000..d8e66c748 --- /dev/null +++ b/backend/src/Squidex.Web/Extensions.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Squidex.Infrastructure.Security; + +namespace Squidex.Web +{ + public static class Extensions + { + public static string? GetClientId(this ClaimsPrincipal principal) + { + var clientId = principal.FindFirst(OpenIdClaims.ClientId)?.Value; + + return clientId?.GetClientParts().ClientId; + } + + public static (string? App, string? ClientId) GetClientParts(this string clientId) + { + var parts = clientId.Split(':', '~'); + + if (parts.Length == 1) + { + return (null, parts[0]); + } + + if (parts.Length == 2) + { + return (parts[0], parts[1]); + } + + return (null, null); + } + + public static bool IsUser(this ApiController controller, string userId) + { + var subject = controller.User.OpenIdSubject(); + + return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase); + } + + public static bool TryGetHeaderString(this IHeaderDictionary headers, string header, [MaybeNullWhen(false)] out string result) + { + if (headers.TryGetValue(header, out var value)) + { + string valueString = value; + + if (!string.IsNullOrWhiteSpace(valueString)) + { + result = valueString; + return true; + } + } + + result = null!; + + return false; + } + } +} diff --git a/backend/src/Squidex.Web/FileCallbackResult.cs b/backend/src/Squidex.Web/FileCallbackResult.cs new file mode 100644 index 000000000..4f1b9f4f7 --- /dev/null +++ b/backend/src/Squidex.Web/FileCallbackResult.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Web.Pipeline; + +namespace Squidex.Web +{ + public sealed class FileCallbackResult : FileResult + { + public bool Send404 { get; } + + public Func Callback { get; } + + public FileCallbackResult(string contentType, string? name, bool send404, Func callback) + : base(contentType) + { + FileDownloadName = name; + + Send404 = send404; + + Callback = callback; + } + + public override Task ExecuteResultAsync(ActionContext context) + { + var executor = context.HttpContext.RequestServices.GetRequiredService(); + + return executor.ExecuteAsync(context, this); + } + } +} + +#pragma warning restore 1573 \ No newline at end of file diff --git a/src/Squidex.Web/FileExtensions.cs b/backend/src/Squidex.Web/FileExtensions.cs similarity index 100% rename from src/Squidex.Web/FileExtensions.cs rename to backend/src/Squidex.Web/FileExtensions.cs diff --git a/src/Squidex.Web/IApiCostsFeature.cs b/backend/src/Squidex.Web/IApiCostsFeature.cs similarity index 100% rename from src/Squidex.Web/IApiCostsFeature.cs rename to backend/src/Squidex.Web/IApiCostsFeature.cs diff --git a/backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs b/backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs new file mode 100644 index 000000000..80e18c23e --- /dev/null +++ b/backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs @@ -0,0 +1,95 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using Newtonsoft.Json.Linq; +using NJsonSchema.Converters; +using Squidex.Infrastructure; + +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Web.Json +{ + public class TypedJsonInheritanceConverter : JsonInheritanceConverter + { + private static readonly Lazy> DefaultMapping = new Lazy>(() => + { + var baseName = typeof(T).Name; + + var result = new Dictionary(); + + void AddType(Type type) + { + var discriminator = type.Name; + + if (discriminator.EndsWith(baseName, StringComparison.CurrentCulture)) + { + discriminator = discriminator.Substring(0, discriminator.Length - baseName.Length); + } + + result[discriminator] = type; + } + + foreach (var attribute in typeof(T).GetCustomAttributes()) + { + if (attribute.Type != null) + { + if (!attribute.Type.IsAbstract) + { + AddType(attribute.Type); + } + } + else if (!string.IsNullOrWhiteSpace(attribute.MethodName)) + { + var method = typeof(T).GetMethod(attribute.MethodName); + + if (method != null && method.IsStatic) + { + var types = (IEnumerable)method.Invoke(null, new object[0])!; + + foreach (var type in types) + { + if (!type.IsAbstract) + { + AddType(type); + } + } + } + } + } + + return result; + }); + + private readonly IReadOnlyDictionary maping; + + public TypedJsonInheritanceConverter(string discriminator) + : this(discriminator, DefaultMapping.Value) + { + } + + public TypedJsonInheritanceConverter(string discriminator, IReadOnlyDictionary mapping) + : base(typeof(T), discriminator) + { + maping = mapping ?? DefaultMapping.Value; + } + + protected override Type GetDiscriminatorType(JObject jObject, Type objectType, string discriminatorValue) + { + return maping.GetOrDefault(discriminatorValue) ?? throw new InvalidOperationException($"Could not find subtype of '{objectType.Name}' with discriminator '{discriminatorValue}'."); + } + + public override string GetDiscriminatorValue(Type type) + { + return maping.FirstOrDefault(x => x.Value == type).Key ?? type.Name; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Web/PermissionExtensions.cs b/backend/src/Squidex.Web/PermissionExtensions.cs new file mode 100644 index 000000000..59f2bf42a --- /dev/null +++ b/backend/src/Squidex.Web/PermissionExtensions.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Http; +using Squidex.Infrastructure.Security; +using AllPermissions = Squidex.Shared.Permissions; + +namespace Squidex.Web +{ + public static class PermissionExtensions + { + public static PermissionSet Permissions(this HttpContext httpContext) + { + return httpContext.Context().Permissions; + } + + public static bool Includes(this HttpContext httpContext, Permission permission, PermissionSet? additional = null) + { + return httpContext.Permissions().Includes(permission) || additional?.Includes(permission) == true; + } + + public static bool Includes(this ApiController controller, Permission permission, PermissionSet? additional = null) + { + return controller.HttpContext.Includes(permission) || additional?.Includes(permission) == true; + } + + public static bool HasPermission(this HttpContext httpContext, Permission permission, PermissionSet? additional = null) + { + return httpContext.Permissions().Allows(permission) || additional?.Allows(permission) == true; + } + + public static bool HasPermission(this ApiController controller, Permission permission, PermissionSet? additional = null) + { + return controller.HttpContext.HasPermission(permission) || additional?.Allows(permission) == true; + } + + public static bool HasPermission(this ApiController controller, string id, string app = Permission.Any, string schema = Permission.Any, PermissionSet? additional = null) + { + if (app == Permission.Any) + { + if (controller.RouteData.Values.TryGetValue("app", out var value) && value is string s) + { + app = s; + } + } + + if (schema == Permission.Any) + { + if (controller.RouteData.Values.TryGetValue("name", out var value) && value is string s) + { + schema = s; + } + } + + var permission = AllPermissions.ForApp(id, app, schema); + + return controller.HasPermission(permission, additional); + } + } +} diff --git a/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs b/backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs similarity index 100% rename from src/Squidex.Web/Pipeline/ActionContextLogAppender.cs rename to backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs diff --git a/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs b/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs new file mode 100644 index 000000000..351245af0 --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs @@ -0,0 +1,88 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.UsageTracking; + +namespace Squidex.Web.Pipeline +{ + public sealed class ApiCostsFilter : IAsyncActionFilter, IFilterContainer + { + private readonly IAppPlansProvider appPlansProvider; + private readonly IUsageTracker usageTracker; + + public ApiCostsFilter(IAppPlansProvider appPlansProvider, IUsageTracker usageTracker) + { + this.appPlansProvider = appPlansProvider; + + this.usageTracker = usageTracker; + } + + IFilterMetadata IFilterContainer.FilterDefinition { get; set; } + + public ApiCostsAttribute FilterDefinition + { + get + { + return (ApiCostsAttribute)((IFilterContainer)this).FilterDefinition; + } + set + { + ((IFilterContainer)this).FilterDefinition = value; + } + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + context.HttpContext.Features.Set(FilterDefinition); + + var app = context.HttpContext.Context().App; + + if (app != null && FilterDefinition.Weight > 0) + { + var appId = app.Id.ToString(); + + using (Profiler.Trace("CheckUsage")) + { + var plan = appPlansProvider.GetPlanForApp(app); + + var usage = await usageTracker.GetMonthlyCallsAsync(appId, DateTime.Today); + + if (plan?.MaxApiCalls >= 0 && usage > plan.MaxApiCalls * 1.1) + { + context.Result = new StatusCodeResult(429); + return; + } + } + + var watch = ValueStopwatch.StartNew(); + + try + { + await next(); + } + finally + { + var elapsedMs = watch.Stop(); + + await usageTracker.TrackAsync(appId, context.HttpContext.User.OpenIdClientId(), FilterDefinition.Weight, elapsedMs); + } + } + else + { + await next(); + } + } + } +} diff --git a/src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs b/backend/src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs similarity index 100% rename from src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs rename to backend/src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs diff --git a/backend/src/Squidex.Web/Pipeline/AppResolver.cs b/backend/src/Squidex.Web/Pipeline/AppResolver.cs new file mode 100644 index 000000000..c15068a06 --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/AppResolver.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Shared.Identity; + +namespace Squidex.Web.Pipeline +{ + public sealed class AppResolver : IAsyncActionFilter + { + private readonly IAppProvider appProvider; + + public AppResolver(IAppProvider appProvider) + { + this.appProvider = appProvider; + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var user = context.HttpContext.User; + + var appName = context.RouteData.Values["app"]?.ToString(); + + if (!string.IsNullOrWhiteSpace(appName)) + { + var app = await appProvider.GetAppAsync(appName); + + if (app == null) + { + context.Result = new NotFoundResult(); + return; + } + + var (role, permissions) = FindByOpenIdSubject(app, user); + + if (permissions == null) + { + (role, permissions) = FindByOpenIdClient(app, user); + } + + if (permissions != null) + { + var identity = user.Identities.First(); + + if (!string.IsNullOrWhiteSpace(role)) + { + identity.AddClaim(new Claim(ClaimTypes.Role, role)); + } + + foreach (var permission in permissions) + { + identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission.Id)); + } + } + + var requestContext = context.HttpContext.Context(); + + requestContext.App = app; + requestContext.UpdatePermissions(); + + if (!requestContext.Permissions.Includes(Permissions.ForApp(Permissions.App, appName)) && !AllowAnonymous(context)) + { + context.Result = new NotFoundResult(); + return; + } + } + + await next(); + } + + private static bool AllowAnonymous(ActionExecutingContext context) + { + return context.ActionDescriptor.EndpointMetadata.Any(x => x is AllowAnonymousAttribute); + } + + private static (string?, PermissionSet?) FindByOpenIdClient(IAppEntity app, ClaimsPrincipal user) + { + var clientId = user.GetClientId(); + + if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && app.Roles.TryGet(app.Name, client.Role, out var role)) + { + return (client.Role, role.Permissions); + } + + return (null, null); + } + + private static (string?, PermissionSet?) FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user) + { + var subjectId = user.OpenIdSubject(); + + if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var roleName) && app.Roles.TryGet(app.Name, roleName, out var role)) + { + return (roleName, role.Permissions); + } + + return (null, null); + } + } +} diff --git a/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs b/backend/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs similarity index 100% rename from src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs rename to backend/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs diff --git a/src/Squidex.Web/Pipeline/DeferredActionFilter.cs b/backend/src/Squidex.Web/Pipeline/DeferredActionFilter.cs similarity index 100% rename from src/Squidex.Web/Pipeline/DeferredActionFilter.cs rename to backend/src/Squidex.Web/Pipeline/DeferredActionFilter.cs diff --git a/src/Squidex.Web/Pipeline/ETagFilter.cs b/backend/src/Squidex.Web/Pipeline/ETagFilter.cs similarity index 100% rename from src/Squidex.Web/Pipeline/ETagFilter.cs rename to backend/src/Squidex.Web/Pipeline/ETagFilter.cs diff --git a/src/Squidex.Web/Pipeline/ETagOptions.cs b/backend/src/Squidex.Web/Pipeline/ETagOptions.cs similarity index 100% rename from src/Squidex.Web/Pipeline/ETagOptions.cs rename to backend/src/Squidex.Web/Pipeline/ETagOptions.cs diff --git a/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs b/backend/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs similarity index 100% rename from src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs rename to backend/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs diff --git a/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs b/backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs similarity index 100% rename from src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs rename to backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs diff --git a/backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs b/backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs new file mode 100644 index 000000000..9498a5dd1 --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; + +namespace Squidex.Web.Pipeline +{ + public sealed class LocalCacheMiddleware : IMiddleware + { + private readonly ILocalCache localCache; + + public LocalCacheMiddleware(ILocalCache localCache) + { + Guard.NotNull(localCache); + + this.localCache = localCache; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + using (localCache.StartContext()) + { + await next(context); + } + } + } +} diff --git a/src/Squidex.Web/Pipeline/MeasureResultFilter.cs b/backend/src/Squidex.Web/Pipeline/MeasureResultFilter.cs similarity index 100% rename from src/Squidex.Web/Pipeline/MeasureResultFilter.cs rename to backend/src/Squidex.Web/Pipeline/MeasureResultFilter.cs diff --git a/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs b/backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs similarity index 100% rename from src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs rename to backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs diff --git a/backend/src/Squidex.Web/Resource.cs b/backend/src/Squidex.Web/Resource.cs new file mode 100644 index 000000000..76d9c62dd --- /dev/null +++ b/backend/src/Squidex.Web/Resource.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; +using Squidex.Infrastructure; + +namespace Squidex.Web +{ + public abstract class Resource + { + [JsonProperty("_links")] + [Required] + [Display(Description = "The links.")] + public Dictionary Links { get; } = new Dictionary(); + + public void AddSelfLink(string href) + { + AddGetLink("self", href); + } + + public void AddGetLink(string rel, string href, string? metadata = null) + { + AddLink(rel, "GET", href, metadata); + } + + public void AddPatchLink(string rel, string href, string? metadata = null) + { + AddLink(rel, "PATCH", href, metadata); + } + + public void AddPostLink(string rel, string href, string? metadata = null) + { + AddLink(rel, "POST", href, metadata); + } + + public void AddPutLink(string rel, string href, string? metadata = null) + { + AddLink(rel, "PUT", href, metadata); + } + + public void AddDeleteLink(string rel, string href, string? metadata = null) + { + AddLink(rel, "DELETE", href, metadata); + } + + public void AddLink(string rel, string method, string href, string? metadata = null) + { + Guard.NotNullOrEmpty(rel); + Guard.NotNullOrEmpty(href); + Guard.NotNullOrEmpty(method); + + Links[rel] = new ResourceLink { Href = href, Method = method, Metadata = metadata }; + } + } +} diff --git a/backend/src/Squidex.Web/ResourceLink.cs b/backend/src/Squidex.Web/ResourceLink.cs new file mode 100644 index 000000000..9003ecb80 --- /dev/null +++ b/backend/src/Squidex.Web/ResourceLink.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; + +namespace Squidex.Web +{ + public class ResourceLink + { + [Required] + [Display(Description = "The link url.")] + public string Href { get; set; } + + [Required] + [Display(Description = "The link method.")] + public string Method { get; set; } + + [Display(Description = "Additional data about the link.")] + public string? Metadata { get; set; } + } +} diff --git a/backend/src/Squidex.Web/Services/UrlGenerator.cs b/backend/src/Squidex.Web/Services/UrlGenerator.cs new file mode 100644 index 000000000..7cbc56f0e --- /dev/null +++ b/backend/src/Squidex.Web/Services/UrlGenerator.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; + +namespace Squidex.Web.Services +{ + public sealed class UrlGenerator : IGraphQLUrlGenerator, IRuleUrlGenerator, IAssetUrlGenerator, IEmailUrlGenerator + { + private readonly IAssetStore assetStore; + private readonly UrlsOptions urlsOptions; + + public bool CanGenerateAssetSourceUrl { get; } + + public UrlGenerator(IOptions urlsOptions, IAssetStore assetStore, bool allowAssetSourceUrl) + { + this.assetStore = assetStore; + this.urlsOptions = urlsOptions.Value; + + CanGenerateAssetSourceUrl = allowAssetSourceUrl; + } + + public string? GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset) + { + if (!asset.IsImage) + { + return null; + } + + return urlsOptions.BuildUrl($"api/assets/{asset.Id}?version={asset.Version}&width=100&mode=Max"); + } + + public string GenerateUrl(string assetId) + { + return urlsOptions.BuildUrl($"api/assets/{assetId}"); + } + + public string GenerateAssetUrl(IAppEntity app, IAssetEntity asset) + { + return urlsOptions.BuildUrl($"api/assets/{asset.Id}?version={asset.Version}"); + } + + public string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content) + { + return urlsOptions.BuildUrl($"api/content/{app.Name}/{schema.SchemaDef.Name}/{content.Id}"); + } + + public string GenerateContentUIUrl(NamedId appId, NamedId schemaId, Guid contentId) + { + return urlsOptions.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}/{contentId}/history"); + } + + public string GenerateUIUrl() + { + return urlsOptions.BuildUrl("app/"); + } + + public string? GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset) + { + return assetStore.GeneratePublicUrl(asset.Id.ToString(), asset.FileVersion, null); + } + } +} diff --git a/backend/src/Squidex.Web/Squidex.Web.csproj b/backend/src/Squidex.Web/Squidex.Web.csproj new file mode 100644 index 000000000..4d6a28a24 --- /dev/null +++ b/backend/src/Squidex.Web/Squidex.Web.csproj @@ -0,0 +1,29 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/src/Squidex.Web/SquidexWeb.cs b/backend/src/Squidex.Web/SquidexWeb.cs similarity index 100% rename from src/Squidex.Web/SquidexWeb.cs rename to backend/src/Squidex.Web/SquidexWeb.cs diff --git a/backend/src/Squidex.Web/UrlHelperExtensions.cs b/backend/src/Squidex.Web/UrlHelperExtensions.cs new file mode 100644 index 000000000..790ecf0f8 --- /dev/null +++ b/backend/src/Squidex.Web/UrlHelperExtensions.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.AspNetCore.Mvc; + +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Web +{ + public static class UrlHelperExtensions + { + private static class NameOf + { + public static readonly string Controller; + + static NameOf() + { + const string suffix = "Controller"; + + var name = typeof(T).Name; + + if (name.EndsWith(suffix, StringComparison.Ordinal)) + { + name = name.Substring(0, name.Length - suffix.Length); + } + + Controller = name; + } + } + + public static string Url(this IUrlHelper urlHelper, Func action, object? values = null) where T : Controller + { + return urlHelper.Action(action(null), NameOf.Controller, values); + } + + public static string Url(this Controller controller, Func action, object? values = null) where T : Controller + { + return controller.Url.Url(action, values); + } + } +} diff --git a/src/Squidex.Web/UrlsOptions.cs b/backend/src/Squidex.Web/UrlsOptions.cs similarity index 100% rename from src/Squidex.Web/UrlsOptions.cs rename to backend/src/Squidex.Web/UrlsOptions.cs diff --git a/src/Squidex.Web/UsageOptions.cs b/backend/src/Squidex.Web/UsageOptions.cs similarity index 100% rename from src/Squidex.Web/UsageOptions.cs rename to backend/src/Squidex.Web/UsageOptions.cs diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs new file mode 100644 index 000000000..20a01d6b7 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Microsoft.Extensions.Options; +using NSwag; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; +using Squidex.Web; + +namespace Squidex.Areas.Api.Config.OpenApi +{ + public sealed class CommonProcessor : IDocumentProcessor + { + private readonly string version; + private readonly string backgroundColor = "#3f83df"; + private readonly string logoUrl; + private readonly OpenApiExternalDocumentation documentation = new OpenApiExternalDocumentation + { + Url = "https://docs.squidex.io" + }; + + public CommonProcessor(ExposedValues exposedValues, IOptions urlOptions) + { + logoUrl = urlOptions.Value.BuildUrl("images/logo-white.png", false); + + if (!exposedValues.TryGetValue("version", out version!) || version == null) + { + version = "1.0"; + } + } + + public void Process(DocumentProcessorContext context) + { + context.Document.BasePath = Constants.ApiPrefix; + + context.Document.Info.Version = version; + context.Document.Info.ExtensionData = new Dictionary + { + ["x-logo"] = new { url = logoUrl, backgroundColor } + }; + + context.Document.ExternalDocumentation = documentation; + } + } +} diff --git a/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs similarity index 100% rename from src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs rename to backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs diff --git a/src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs similarity index 100% rename from src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs rename to backend/src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs diff --git a/src/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs similarity index 100% rename from src/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs rename to backend/src/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs diff --git a/src/Squidex/Areas/Api/Config/OpenApi/ODataQueryParamsProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ODataQueryParamsProcessor.cs similarity index 100% rename from src/Squidex/Areas/Api/Config/OpenApi/ODataQueryParamsProcessor.cs rename to backend/src/Squidex/Areas/Api/Config/OpenApi/ODataQueryParamsProcessor.cs diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiExtensions.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiExtensions.cs new file mode 100644 index 000000000..5dfc0dc69 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiExtensions.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; + +namespace Squidex.Areas.Api.Config.OpenApi +{ + public static class OpenApiExtensions + { + public static void UseSquidexOpenApi(this IApplicationBuilder app) + { + app.UseOpenApi(); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs new file mode 100644 index 000000000..0fafa6ba0 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using NJsonSchema; +using NJsonSchema.Generation.TypeMappers; +using NodaTime; +using NSwag.Generation; +using NSwag.Generation.Processors; +using Squidex.Areas.Api.Controllers.Contents.Generator; +using Squidex.Areas.Api.Controllers.Rules.Models; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Areas.Api.Config.OpenApi +{ + public static class OpenApiServices + { + public static void AddSquidexOpenApiSettings(this IServiceCollection services) + { + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddOpenApiDocument(settings => + { + settings.ConfigureName(); + settings.ConfigureSchemaSettings(); + + settings.OperationProcessors.Add(new ODataQueryParamsProcessor("/apps/{app}/assets", "assets", false)); + }); + + services.AddTransient(); + } + + public static void ConfigureName(this T settings) where T : OpenApiDocumentGeneratorSettings + { + settings.Title = "Squidex API"; + } + + public static void ConfigureSchemaSettings(this T settings) where T : OpenApiDocumentGeneratorSettings + { + settings.TypeMappers = new List + { + new PrimitiveTypeMapper(typeof(Instant), schema => + { + schema.Type = JsonObjectType.String; + schema.Format = JsonFormatStrings.DateTime; + }), + + new PrimitiveTypeMapper(typeof(Language), s => s.Type = JsonObjectType.String), + new PrimitiveTypeMapper(typeof(RefToken), s => s.Type = JsonObjectType.String), + new PrimitiveTypeMapper(typeof(Status), s => s.Type = JsonObjectType.String) + }; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs new file mode 100644 index 000000000..c7043f120 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Authorization; +using NSwag; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; +using Squidex.Web; + +namespace Squidex.Areas.Api.Config.OpenApi +{ + public sealed class ScopesProcessor : IOperationProcessor + { + public bool Process(OperationProcessorContext context) + { + if (context.OperationDescription.Operation.Security == null) + { + context.OperationDescription.Operation.Security = new List(); + } + + var permissionAttribute = context.MethodInfo.GetCustomAttribute(); + + if (permissionAttribute != null) + { + context.OperationDescription.Operation.Security.Add(new OpenApiSecurityRequirement + { + [Constants.SecurityDefinition] = permissionAttribute.PermissionIds + }); + } + else + { + var authorizeAttributes = + context.MethodInfo.GetCustomAttributes(true).Union( + context.MethodInfo.DeclaringType!.GetCustomAttributes(true)) + .ToArray(); + + if (authorizeAttributes.Any()) + { + var scopes = authorizeAttributes.Where(a => a.Roles != null).SelectMany(a => a.Roles.Split(',')).Distinct().ToList(); + + context.OperationDescription.Operation.Security.Add(new OpenApiSecurityRequirement + { + [Constants.SecurityDefinition] = scopes + }); + } + } + + return true; + } + } +} diff --git a/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs similarity index 100% rename from src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs rename to backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs diff --git a/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs similarity index 100% rename from src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs rename to backend/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs new file mode 100644 index 000000000..f2bf8eb99 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Text.RegularExpressions; +using Namotion.Reflection; +using NSwag; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; + +namespace Squidex.Areas.Api.Config.OpenApi +{ + public sealed class XmlResponseTypesProcessor : IOperationProcessor + { + private static readonly Regex ResponseRegex = new Regex("(?[0-9]{3}) => (?.*)", RegexOptions.Compiled); + + public bool Process(OperationProcessorContext context) + { + var operation = context.OperationDescription.Operation; + + var returnsDescription = context.MethodInfo.GetXmlDocsTag("returns"); + + if (!string.IsNullOrWhiteSpace(returnsDescription)) + { + foreach (var match in ResponseRegex.Matches(returnsDescription).OfType()) + { + var statusCode = match.Groups["Code"].Value; + + if (!operation.Responses.TryGetValue(statusCode, out var response)) + { + response = new OpenApiResponse(); + + operation.Responses[statusCode] = response; + } + + var description = match.Groups["Description"].Value; + + if (description.Contains("=>")) + { + throw new InvalidOperationException("Description not formatted correcly."); + } + + response.Description = description; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs new file mode 100644 index 000000000..ce46335c1 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Namotion.Reflection; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; + +namespace Squidex.Areas.Api.Config.OpenApi +{ + public sealed class XmlTagProcessor : IDocumentProcessor + { + public void Process(DocumentProcessorContext context) + { + foreach (var controllerType in context.ControllerTypes) + { + var attribute = controllerType.GetCustomAttribute(); + + if (attribute != null) + { + var tag = context.Document.Tags.FirstOrDefault(x => x.Name == attribute.GroupName); + + if (tag != null) + { + var description = controllerType.GetXmlDocsSummary(); + + if (description != null) + { + tag.Description ??= string.Empty; + + if (!tag.Description.Contains(description)) + { + tag.Description += "\n\n" + description; + } + } + } + } + } + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs new file mode 100644 index 000000000..4ce50f255 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -0,0 +1,302 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using NSwag.Annotations; +using Squidex.Areas.Api.Controllers.Apps.Models; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.Validation; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Apps +{ + /// + /// Manages and configures apps. + /// + [ApiExplorerSettings(GroupName = nameof(Apps))] + public sealed class AppsController : ApiController + { + private readonly IAssetStore assetStore; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly IAppProvider appProvider; + private readonly IAppPlansProvider appPlansProvider; + + public AppsController(ICommandBus commandBus, + IAssetStore assetStore, + IAssetThumbnailGenerator assetThumbnailGenerator, + IAppProvider appProvider, + IAppPlansProvider appPlansProvider) + : base(commandBus) + { + this.assetStore = assetStore; + this.assetThumbnailGenerator = assetThumbnailGenerator; + this.appProvider = appProvider; + this.appPlansProvider = appPlansProvider; + } + + /// + /// Get your apps. + /// + /// + /// 200 => Apps returned. + /// + /// + /// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow). + /// You will retrieve all apps, where you are assigned as a contributor. + /// + [HttpGet] + [Route("apps/")] + [ProducesResponseType(typeof(AppDto[]), 200)] + [ApiPermission] + [ApiCosts(0)] + public async Task GetApps() + { + var userOrClientId = HttpContext.User.UserOrClientId()!; + var userPermissions = HttpContext.Permissions(); + + var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions); + + var response = Deferred.Response(() => + { + return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider, this)).ToArray(); + }); + + Response.Headers[HeaderNames.ETag] = apps.ToEtag(); + + return Ok(response); + } + + /// + /// Create a new app. + /// + /// The app object that needs to be added to squidex. + /// + /// 201 => App created. + /// 400 => App request not valid. + /// 409 => App name is already in use. + /// + /// + /// You can only create an app when you are authenticated as a user (OpenID implicit flow). + /// You will be assigned as owner of the new app automatically. + /// + [HttpPost] + [Route("apps/")] + [ProducesResponseType(typeof(AppDto), 201)] + [ApiPermission] + [ApiCosts(0)] + public async Task PostApp([FromBody] CreateAppDto request) + { + var response = await InvokeCommandAsync(request.ToCommand()); + + return CreatedAtAction(nameof(GetApps), response); + } + + /// + /// Update the app. + /// + /// The name of the app to update. + /// The values to update. + /// + /// 200 => App updated. + /// 404 => App not found. + /// + [HttpPut] + [Route("apps/{app}/")] + [ProducesResponseType(typeof(AppDto), 200)] + [ApiPermission(Permissions.AppUpdateGeneral)] + [ApiCosts(0)] + public async Task UpdateApp(string app, [FromBody] UpdateAppDto request) + { + var response = await InvokeCommandAsync(request.ToCommand()); + + return Ok(response); + } + + /// + /// Get the app image. + /// + /// The name of the app to update. + /// The file to upload. + /// + /// 200 => App image uploaded. + /// 404 => App not found. + /// + [HttpPost] + [Route("apps/{app}/image")] + [ProducesResponseType(typeof(AppDto), 201)] + [ApiPermission(Permissions.AppUpdateImage)] + [ApiCosts(0)] + public async Task UploadImage(string app, [OpenApiIgnore] List file) + { + var response = await InvokeCommandAsync(CreateCommand(file)); + + return Ok(response); + } + + /// + /// Get the app image. + /// + /// The name of the app. + /// + /// 200 => App image found and content or (resized) image returned. + /// 404 => App not found. + /// + [HttpGet] + [Route("apps/{app}/image")] + [ProducesResponseType(typeof(FileResult), 200)] + [AllowAnonymous] + [ApiCosts(0)] + public IActionResult GetImage(string app) + { + if (App.Image == null) + { + return NotFound(); + } + + var etag = App.Image.Etag; + + Response.Headers[HeaderNames.ETag] = etag; + + var handler = new Func(async bodyStream => + { + var assetId = App.Id.ToString(); + var assetResizedId = $"{assetId}_{etag}_Resized"; + + try + { + await assetStore.DownloadAsync(assetResizedId, bodyStream); + } + catch (AssetNotFoundException) + { + using (Profiler.Trace("Resize")) + { + using (var sourceStream = GetTempStream()) + { + using (var destinationStream = GetTempStream()) + { + using (Profiler.Trace("ResizeDownload")) + { + await assetStore.DownloadAsync(assetId, sourceStream); + sourceStream.Position = 0; + } + + using (Profiler.Trace("ResizeImage")) + { + await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, 150, 150, "Crop"); + destinationStream.Position = 0; + } + + using (Profiler.Trace("ResizeUpload")) + { + await assetStore.UploadAsync(assetResizedId, destinationStream); + destinationStream.Position = 0; + } + + await destinationStream.CopyToAsync(bodyStream); + } + } + } + } + }); + + return new FileCallbackResult(App.Image.MimeType, null, true, handler); + } + + /// + /// Remove the app image. + /// + /// The name of the app to update. + /// + /// 200 => App image removed. + /// 404 => App not found. + /// + [HttpDelete] + [Route("apps/{app}/image")] + [ProducesResponseType(typeof(AppDto), 201)] + [ApiPermission(Permissions.AppUpdate)] + [ApiCosts(0)] + public async Task DeleteImage(string app) + { + var response = await InvokeCommandAsync(new RemoveAppImage()); + + return Ok(response); + } + + /// + /// Archive the app. + /// + /// The name of the app to archive. + /// + /// 204 => App archived. + /// 404 => App not found. + /// + [HttpDelete] + [Route("apps/{app}/")] + [ApiPermission(Permissions.AppDelete)] + [ApiCosts(0)] + public async Task DeleteApp(string app) + { + await CommandBus.PublishAsync(new ArchiveApp()); + + return NoContent(); + } + + private async Task InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command); + + var userOrClientId = HttpContext.User.UserOrClientId()!; + var userPermissions = HttpContext.Permissions(); + + var result = context.Result(); + var response = AppDto.FromApp(result, userOrClientId, userPermissions, appPlansProvider, this); + + return response; + } + + private static UploadAppImage CreateCommand(IReadOnlyList file) + { + if (file.Count != 1) + { + var error = new ValidationError($"Can only upload one file, found {file.Count} files."); + + throw new ValidationException("Cannot create asset.", error); + } + + return new UploadAppImage { File = file[0].ToAssetFile() }; + } + + private static FileStream GetTempStream() + { + var tempFileName = Path.GetTempFileName(); + + return new FileStream(tempFileName, + FileMode.Create, + FileAccess.ReadWrite, + FileShare.Delete, 1024 * 16, + FileOptions.Asynchronous | + FileOptions.DeleteOnClose | + FileOptions.SequentialScan); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs new file mode 100644 index 000000000..8192b050b --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -0,0 +1,244 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using NodaTime; +using Squidex.Areas.Api.Controllers.Assets; +using Squidex.Areas.Api.Controllers.Backups; +using Squidex.Areas.Api.Controllers.Ping; +using Squidex.Areas.Api.Controllers.Plans; +using Squidex.Areas.Api.Controllers.Rules; +using Squidex.Areas.Api.Controllers.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Web; +using AllPermissions = Squidex.Shared.Permissions; + +#pragma warning disable RECS0033 // Convert 'if' to '||' expression + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class AppDto : Resource + { + /// + /// The name of the app. + /// + [Required] + [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] + public string Name { get; set; } + + /// + /// The optional label of the app. + /// + public string Label { get; set; } + + /// + /// The optional description of the app. + /// + public string Description { get; set; } + + /// + /// The version of the app. + /// + public long Version { get; set; } + + /// + /// The id of the app. + /// + public Guid Id { get; set; } + + /// + /// The timestamp when the app has been created. + /// + public Instant Created { get; set; } + + /// + /// The timestamp when the app has been modified last. + /// + public Instant LastModified { get; set; } + + /// + /// The permission level of the user. + /// + public IEnumerable Permissions { get; set; } + + /// + /// Indicates if the user can access the api. + /// + public bool CanAccessApi { get; set; } + + /// + /// Indicates if the user can access at least one content. + /// + public bool CanAccessContent { get; set; } + + /// + /// Gets the current plan name. + /// + public string? PlanName { get; set; } + + /// + /// Gets the next plan name. + /// + public string? PlanUpgrade { get; set; } + + public static AppDto FromApp(IAppEntity app, string userId, PermissionSet userPermissions, IAppPlansProvider plans, ApiController controller) + { + var permissions = GetPermissions(app, userId, userPermissions); + + var result = SimpleMapper.Map(app, new AppDto()); + + result.Permissions = permissions.ToIds(); + + if (controller.Includes(AllPermissions.ForApp(AllPermissions.AppApi, app.Name), permissions)) + { + result.CanAccessApi = true; + } + + if (controller.Includes(AllPermissions.ForApp(AllPermissions.AppContents, app.Name), permissions)) + { + result.CanAccessContent = true; + } + + result.SetPlan(app, plans, controller, permissions); + result.SetImage(app, controller); + + return result.CreateLinks(controller, permissions); + } + + private static PermissionSet GetPermissions(IAppEntity app, string userId, PermissionSet userPermissions) + { + var permissions = new List(); + + if (app.Contributors.TryGetValue(userId, out var roleName) && app.Roles.TryGet(app.Name, roleName, out var role)) + { + permissions.AddRange(role.Permissions); + } + + if (userPermissions != null) + { + permissions.AddRange(userPermissions.ToAppPermissions(app.Name)); + } + + return new PermissionSet(permissions); + } + + private void SetPlan(IAppEntity app, IAppPlansProvider plans, ApiController controller, PermissionSet permissions) + { + if (controller.HasPermission(AllPermissions.AppPlansChange, app.Name, additional: permissions)) + { + PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name; + } + + PlanName = plans.GetPlanForApp(app).Name; + } + + private void SetImage(IAppEntity app, ApiController controller) + { + if (app.Image != null) + { + AddGetLink("image", controller.Url(x => nameof(x.GetImage), new { app = app.Name })); + } + } + + private AppDto CreateLinks(ApiController controller, PermissionSet permissions) + { + var values = new { app = Name }; + + AddGetLink("ping", controller.Url(x => nameof(x.GetAppPing), values)); + + if (controller.HasPermission(AllPermissions.AppDelete, Name, additional: permissions)) + { + AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteApp), values)); + } + + if (controller.HasPermission(AllPermissions.AppUpdateGeneral, Name, additional: permissions)) + { + AddPutLink("update", controller.Url(x => nameof(x.UpdateApp), values)); + } + + if (controller.HasPermission(AllPermissions.AppUpdateImage, Name, additional: permissions)) + { + AddPostLink("image/upload", controller.Url(x => nameof(x.UploadImage), values)); + + AddDeleteLink("image/delete", controller.Url(x => nameof(x.DeleteImage), values)); + } + + if (controller.HasPermission(AllPermissions.AppAssetsRead, Name, additional: permissions)) + { + AddGetLink("assets", controller.Url(x => nameof(x.GetAssets), values)); + } + + if (controller.HasPermission(AllPermissions.AppBackupsRead, Name, additional: permissions)) + { + AddGetLink("backups", controller.Url(x => nameof(x.GetBackups), values)); + } + + if (controller.HasPermission(AllPermissions.AppClientsRead, Name, additional: permissions)) + { + AddGetLink("clients", controller.Url(x => nameof(x.GetClients), values)); + } + + if (controller.HasPermission(AllPermissions.AppContributorsRead, Name, additional: permissions)) + { + AddGetLink("contributors", controller.Url(x => nameof(x.GetContributors), values)); + } + + if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) + { + AddGetLink("languages", controller.Url(x => nameof(x.GetLanguages), values)); + } + + if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) + { + AddGetLink("patterns", controller.Url(x => nameof(x.GetPatterns), values)); + } + + if (controller.HasPermission(AllPermissions.AppPlansRead, Name, additional: permissions)) + { + AddGetLink("plans", controller.Url(x => nameof(x.GetPlans), values)); + } + + if (controller.HasPermission(AllPermissions.AppRolesRead, Name, additional: permissions)) + { + AddGetLink("roles", controller.Url(x => nameof(x.GetRoles), values)); + } + + if (controller.HasPermission(AllPermissions.AppRulesRead, Name, additional: permissions)) + { + AddGetLink("rules", controller.Url(x => nameof(x.GetRules), values)); + } + + if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) + { + AddGetLink("schemas", controller.Url(x => nameof(x.GetSchemas), values)); + } + + if (controller.HasPermission(AllPermissions.AppWorkflowsRead, Name, additional: permissions)) + { + AddGetLink("workflows", controller.Url(x => nameof(x.GetWorkflows), values)); + } + + if (controller.HasPermission(AllPermissions.AppSchemasCreate, Name, additional: permissions)) + { + AddPostLink("schemas/create", controller.Url(x => nameof(x.PostSchema), values)); + } + + if (controller.HasPermission(AllPermissions.AppAssetsCreate, Name, additional: permissions)) + { + AddPostLink("assets/create", controller.Url(x => nameof(x.PostSchema), values)); + } + + return this; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs new file mode 100644 index 000000000..cfad52a07 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs @@ -0,0 +1,76 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Squidex.Shared; +using Squidex.Shared.Users; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class ContributorDto : Resource + { + private const string NotFound = "- not found -"; + + /// + /// The id of the user that contributes to the app. + /// + [Required] + public string ContributorId { get; set; } + + /// + /// The display name. + /// + [Required] + public string ContributorName { get; set; } + + /// + /// The role of the contributor. + /// + public string Role { get; set; } + + public static ContributorDto FromIdAndRole(string id, string role) + { + var result = new ContributorDto { ContributorId = id, Role = role }; + + return result; + } + + public ContributorDto WithUser(IDictionary users) + { + if (users.TryGetValue(ContributorId, out var user)) + { + ContributorName = user.DisplayName()!; + } + else + { + ContributorName = NotFound; + } + + return this; + } + + public ContributorDto WithLinks(ApiController controller, string app) + { + if (!controller.IsUser(ContributorId)) + { + if (controller.HasPermission(Permissions.AppContributorsAssign, app)) + { + AddPostLink("update", controller.Url(x => nameof(x.PostContributor), new { app })); + } + + if (controller.HasPermission(Permissions.AppContributorsRevoke, app)) + { + AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteContributor), new { app, id = ContributorId })); + } + } + + return this; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsMetadata.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsMetadata.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsMetadata.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsMetadata.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs new file mode 100644 index 000000000..c930cf146 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps.Commands; + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class UpdateWorkflowDto + { + /// + /// The name of the workflow. + /// + public string Name { get; set; } + + /// + /// The workflow steps. + /// + [Required] + public Dictionary Steps { get; set; } + + /// + /// The schema ids. + /// + public List SchemaIds { get; set; } + + /// + /// The initial step. + /// + public Status Initial { get; set; } + + public UpdateWorkflow ToCommand(Guid id) + { + var workflow = new Workflow( + Initial, + Steps?.ToDictionary( + x => x.Key, + x => x.Value?.ToStep()!), + SchemaIds, + Name); + + return new UpdateWorkflow { WorkflowId = id, Workflow = workflow }; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs new file mode 100644 index 000000000..56e51c565 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class WorkflowDto : Resource + { + /// + /// The workflow id. + /// + public Guid Id { get; set; } + + /// + /// The name of the workflow. + /// + public string Name { get; set; } + + /// + /// The workflow steps. + /// + [Required] + public Dictionary Steps { get; set; } + + /// + /// The schema ids. + /// + public IReadOnlyList SchemaIds { get; set; } + + /// + /// The initial step. + /// + public Status Initial { get; set; } + + public static WorkflowDto FromWorkflow(Guid id, Workflow workflow) + { + var result = SimpleMapper.Map(workflow, new WorkflowDto + { + Steps = workflow.Steps.ToDictionary( + x => x.Key, + x => WorkflowStepDto.FromWorkflowStep(x.Value)!), + Id = id + }); + + return result; + } + + public WorkflowDto WithLinks(ApiController controller, string app) + { + var values = new { app, id = Id }; + + if (controller.HasPermission(Permissions.AppWorkflowsUpdate, app)) + { + AddPutLink("update", controller.Url(x => nameof(x.PutWorkflow), values)); + } + + if (controller.HasPermission(Permissions.AppWorkflowsDelete, app)) + { + AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteWorkflow), values)); + } + + return this; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs new file mode 100644 index 000000000..9ee0ecaf5 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class WorkflowStepDto + { + /// + /// The transitions. + /// + [Required] + public Dictionary Transitions { get; set; } + + /// + /// The optional color. + /// + public string Color { get; set; } + + /// + /// Indicates if updates should not be allowed. + /// + public bool NoUpdate { get; set; } + + public static WorkflowStepDto? FromWorkflowStep(WorkflowStep step) + { + if (step == null) + { + return null; + } + + return SimpleMapper.Map(step, new WorkflowStepDto + { + Transitions = step.Transitions.ToDictionary( + y => y.Key, + y => WorkflowTransitionDto.FromWorkflowTransition(y.Value)!) + }); + } + + public WorkflowStep ToStep() + { + return new WorkflowStep( + Transitions?.ToDictionary( + y => y.Key, + y => y.Value?.ToTransition()!), + Color, NoUpdate); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs new file mode 100644 index 000000000..a76650d40 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.ObjectModel; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class WorkflowTransitionDto + { + /// + /// The optional expression. + /// + public string? Expression { get; set; } + + /// + /// The optional restricted role. + /// + public ReadOnlyCollection? Roles { get; set; } + + public static WorkflowTransitionDto? FromWorkflowTransition(WorkflowTransition transition) + { + if (transition == null) + { + return null; + } + + return new WorkflowTransitionDto { Expression = transition.Expression, Roles = transition.Roles }; + } + + public WorkflowTransition ToTransition() + { + return new WorkflowTransition(Expression, Roles); + } + } +} \ No newline at end of file diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs new file mode 100644 index 000000000..a7c5a4eec --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -0,0 +1,202 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using Squidex.Areas.Api.Controllers.Assets.Models; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Web; + +#pragma warning disable 1573 + +namespace Squidex.Areas.Api.Controllers.Assets +{ + /// + /// Uploads and retrieves assets. + /// + [ApiExplorerSettings(GroupName = nameof(Assets))] + public sealed class AssetContentController : ApiController + { + private readonly IAssetStore assetStore; + private readonly IAssetRepository assetRepository; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + + public AssetContentController( + ICommandBus commandBus, + IAssetStore assetStore, + IAssetRepository assetRepository, + IAssetThumbnailGenerator assetThumbnailGenerator) + : base(commandBus) + { + this.assetStore = assetStore; + this.assetRepository = assetRepository; + this.assetThumbnailGenerator = assetThumbnailGenerator; + } + + /// + /// Get the asset content. + /// + /// The name of the app. + /// The id or slug of the asset. + /// Optional suffix that can be used to seo-optimize the link to the image Has not effect. + /// The query string parameters. + /// + /// 200 => Asset found and content or (resized) image returned. + /// 404 => Asset or app not found. + /// + [HttpGet] + [Route("assets/{app}/{idOrSlug}/{*more}")] + [ProducesResponseType(typeof(FileResult), 200)] + [ApiCosts(0.5)] + [AllowAnonymous] + public async Task GetAssetContentBySlug(string app, string idOrSlug, string more, [FromQuery] AssetQuery query) + { + IAssetEntity? asset; + + if (Guid.TryParse(idOrSlug, out var guid)) + { + asset = await assetRepository.FindAssetAsync(guid); + } + else + { + asset = await assetRepository.FindAssetBySlugAsync(App.Id, idOrSlug); + } + + return DeliverAsset(asset, query); + } + + /// + /// Get the asset content. + /// + /// The id of the asset. + /// The query string parameters. + /// + /// 200 => Asset found and content or (resized) image returned. + /// 404 => Asset or app not found. + /// + [HttpGet] + [Route("assets/{id}/")] + [ProducesResponseType(typeof(FileResult), 200)] + [ApiCosts(0.5)] + public async Task GetAssetContent(Guid id, [FromQuery] AssetQuery query) + { + var asset = await assetRepository.FindAssetAsync(id); + + return DeliverAsset(asset, query); + } + + private IActionResult DeliverAsset(IAssetEntity? asset, AssetQuery query) + { + query ??= new AssetQuery(); + + if (asset == null || asset.FileVersion < query.Version) + { + return NotFound(); + } + + var fileVersion = query.Version; + + if (fileVersion <= EtagVersion.Any) + { + fileVersion = asset.FileVersion; + } + + Response.Headers[HeaderNames.ETag] = fileVersion.ToString(); + + if (query.CacheDuration > 0) + { + Response.Headers[HeaderNames.CacheControl] = $"public,max-age={query.CacheDuration}"; + } + + var handler = new Func(async bodyStream => + { + var assetId = asset.Id.ToString(); + + if (asset.IsImage && query.ShouldResize()) + { + var assetSuffix = $"{query.Width}_{query.Height}_{query.Mode}"; + + if (query.Quality.HasValue) + { + assetSuffix += $"_{query.Quality}"; + } + + try + { + await assetStore.DownloadAsync(assetId, fileVersion, assetSuffix, bodyStream); + } + catch (AssetNotFoundException) + { + using (Profiler.Trace("Resize")) + { + using (var sourceStream = GetTempStream()) + { + using (var destinationStream = GetTempStream()) + { + using (Profiler.Trace("ResizeDownload")) + { + await assetStore.DownloadAsync(assetId, fileVersion, null, sourceStream); + sourceStream.Position = 0; + } + + using (Profiler.Trace("ResizeImage")) + { + await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, query.Width, query.Height, query.Mode, query.Quality); + destinationStream.Position = 0; + } + + using (Profiler.Trace("ResizeUpload")) + { + await assetStore.UploadAsync(assetId, fileVersion, assetSuffix, destinationStream); + destinationStream.Position = 0; + } + + await destinationStream.CopyToAsync(bodyStream); + } + } + } + } + } + else + { + await assetStore.DownloadAsync(assetId, fileVersion, null, bodyStream); + } + }); + + if (query.Download == 1) + { + return new FileCallbackResult(asset.MimeType, asset.FileName, true, handler); + } + else + { + return new FileCallbackResult(asset.MimeType, null, true, handler); + } + } + + private static FileStream GetTempStream() + { + var tempFileName = Path.GetTempFileName(); + + return new FileStream(tempFileName, + FileMode.Create, + FileAccess.ReadWrite, + FileShare.Delete, 1024 * 16, + FileOptions.Asynchronous | + FileOptions.DeleteOnClose | + FileOptions.SequentialScan); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs new file mode 100644 index 000000000..7a7feca6e --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -0,0 +1,319 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using NSwag.Annotations; +using Squidex.Areas.Api.Controllers.Assets.Models; +using Squidex.Areas.Api.Controllers.Contents; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Validation; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Assets +{ + /// + /// Uploads and retrieves assets. + /// + [ApiExplorerSettings(GroupName = nameof(Assets))] + public sealed class AssetsController : ApiController + { + private readonly IAssetQueryService assetQuery; + private readonly IAssetUsageTracker assetStatsRepository; + private readonly IAppPlansProvider appPlansProvider; + private readonly MyContentsControllerOptions controllerOptions; + private readonly ITagService tagService; + private readonly AssetOptions assetOptions; + + public AssetsController( + ICommandBus commandBus, + IAssetQueryService assetQuery, + IAssetUsageTracker assetStatsRepository, + IAppPlansProvider appPlansProvider, + IOptions assetOptions, + IOptions controllerOptions, + ITagService tagService) + : base(commandBus) + { + this.assetOptions = assetOptions.Value; + this.assetQuery = assetQuery; + this.assetStatsRepository = assetStatsRepository; + this.appPlansProvider = appPlansProvider; + this.controllerOptions = controllerOptions.Value; + this.tagService = tagService; + } + + /// + /// Get assets tags. + /// + /// The name of the app. + /// + /// 200 => Assets returned. + /// 404 => App not found. + /// + /// + /// Get all tags for assets. + /// + [HttpGet] + [Route("apps/{app}/assets/tags")] + [ProducesResponseType(typeof(Dictionary), 200)] + [ApiPermission(Permissions.AppAssetsRead)] + [ApiCosts(1)] + public async Task GetTags(string app) + { + var tags = await tagService.GetTagsAsync(AppId, TagGroups.Assets); + + Response.Headers[HeaderNames.ETag] = tags.Version.ToString(); + + return Ok(tags); + } + + /// + /// Get assets. + /// + /// The name of the app. + /// The optional asset ids. + /// The optional json query. + /// + /// 200 => Assets returned. + /// 404 => App not found. + /// + /// + /// Get all assets for the app. + /// + [HttpGet] + [Route("apps/{app}/assets/")] + [ProducesResponseType(typeof(AssetsDto), 200)] + [ApiPermission(Permissions.AppAssetsRead)] + [ApiCosts(1)] + public async Task GetAssets(string app, [FromQuery] string? ids = null, [FromQuery] string? q = null) + { + var assets = await assetQuery.QueryAsync(Context, + Q.Empty + .WithIds(ids) + .WithJsonQuery(q) + .WithODataQuery(Request.QueryString.ToString())); + + var response = Deferred.Response(() => + { + return AssetsDto.FromAssets(assets, this, app); + }); + + if (controllerOptions.EnableSurrogateKeys && assets.Count <= controllerOptions.MaxItemsForSurrogateKeys) + { + Response.Headers["Surrogate-Key"] = assets.ToSurrogateKeys(); + } + + Response.Headers[HeaderNames.ETag] = assets.ToEtag(); + + return Ok(response); + } + + /// + /// Get an asset by id. + /// + /// The name of the app. + /// The id of the asset to retrieve. + /// + /// 200 => Asset found. + /// 404 => Asset or app not found. + /// + [HttpGet] + [Route("apps/{app}/assets/{id}/")] + [ProducesResponseType(typeof(AssetsDto), 200)] + [ApiPermission(Permissions.AppAssetsRead)] + [ApiCosts(1)] + public async Task GetAsset(string app, Guid id) + { + var asset = await assetQuery.FindAssetAsync(Context, id); + + if (asset == null) + { + return NotFound(); + } + + var response = Deferred.Response(() => + { + return AssetDto.FromAsset(asset, this, app); + }); + + if (controllerOptions.EnableSurrogateKeys) + { + Response.Headers["Surrogate-Key"] = asset.ToSurrogateKey(); + } + + Response.Headers[HeaderNames.ETag] = asset.ToEtag(); + + return Ok(response); + } + + /// + /// Upload a new asset. + /// + /// The name of the app. + /// The file to upload. + /// + /// 201 => Asset created. + /// 404 => App not found. + /// 400 => Asset exceeds the maximum size. + /// + /// + /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. + /// + [HttpPost] + [Route("apps/{app}/assets/")] + [ProducesResponseType(typeof(AssetDto), 201)] + [AssetRequestSizeLimit] + [ApiPermission(Permissions.AppAssetsCreate)] + [ApiCosts(1)] + public async Task PostAsset(string app, [OpenApiIgnore] List file) + { + var assetFile = await CheckAssetFileAsync(file); + + var command = new CreateAsset { File = assetFile }; + + var response = await InvokeCommandAsync(app, command); + + return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); + } + + /// + /// Replace asset content. + /// + /// The name of the app. + /// The id of the asset. + /// The file to upload. + /// + /// 200 => Asset updated. + /// 404 => Asset or app not found. + /// 400 => Asset exceeds the maximum size. + /// + /// + /// Use multipart request to upload an asset. + /// + [HttpPut] + [Route("apps/{app}/assets/{id}/content/")] + [ProducesResponseType(typeof(AssetDto), 200)] + [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task PutAssetContent(string app, Guid id, [OpenApiIgnore] List file) + { + var assetFile = await CheckAssetFileAsync(file); + + var command = new UpdateAsset { File = assetFile, AssetId = id }; + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Updates the asset. + /// + /// The name of the app. + /// The id of the asset. + /// The asset object that needs to updated. + /// + /// 200 => Asset updated. + /// 400 => Asset name not valid. + /// 404 => Asset or app not found. + /// + [HttpPut] + [Route("apps/{app}/assets/{id}/")] + [ProducesResponseType(typeof(AssetDto), 200)] + [AssetRequestSizeLimit] + [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task PutAsset(string app, Guid id, [FromBody] AnnotateAssetDto request) + { + var command = request.ToCommand(id); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Delete an asset. + /// + /// The name of the app. + /// The id of the asset to delete. + /// + /// 204 => Asset deleted. + /// 404 => Asset or app not found. + /// + [HttpDelete] + [Route("apps/{app}/assets/{id}/")] + [ApiPermission(Permissions.AppAssetsDelete)] + [ApiCosts(1)] + public async Task DeleteAsset(string app, Guid id) + { + await CommandBus.PublishAsync(new DeleteAsset { AssetId = id }); + + return NoContent(); + } + + private async Task InvokeCommandAsync(string app, ICommand command) + { + var context = await CommandBus.PublishAsync(command); + + if (context.PlainResult is AssetCreatedResult created) + { + return AssetDto.FromAsset(created.Asset, this, app, created.IsDuplicate); + } + else + { + return AssetDto.FromAsset(context.Result(), this, app); + } + } + + private async Task CheckAssetFileAsync(IReadOnlyList file) + { + if (file.Count != 1) + { + var error = new ValidationError($"Can only upload one file, found {file.Count} files."); + + throw new ValidationException("Cannot create asset.", error); + } + + var formFile = file[0]; + + if (formFile.Length > assetOptions.MaxSize) + { + var error = new ValidationError($"File cannot be bigger than {assetOptions.MaxSize.ToReadableSize()}."); + + throw new ValidationException("Cannot create asset.", error); + } + + var plan = appPlansProvider.GetPlanForApp(App); + + var currentSize = await assetStatsRepository.GetTotalSizeAsync(AppId); + + if (plan.MaxAssetSize > 0 && plan.MaxAssetSize < currentSize + formFile.Length) + { + var error = new ValidationError("You have reached your max asset size."); + + throw new ValidationException("Cannot create asset.", error); + } + + return formFile.ToAssetFile(); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMetadata.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMetadata.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMetadata.cs rename to backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMetadata.cs diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetQuery.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetQuery.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Assets/Models/AssetQuery.cs rename to backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetQuery.cs diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreJobDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreJobDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreJobDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs new file mode 100644 index 000000000..43e621981 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Orleans; +using Squidex.Areas.Api.Controllers.Backups.Models; +using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Backups +{ + /// + /// Manages backups for apps. + /// + [ApiExplorerSettings(GroupName = nameof(Backups))] + public class RestoreController : ApiController + { + private readonly IGrainFactory grainFactory; + + public RestoreController(ICommandBus commandBus, IGrainFactory grainFactory) + : base(commandBus) + { + this.grainFactory = grainFactory; + } + + /// + /// Get current restore status. + /// + /// + /// 200 => Status returned. + /// + [HttpGet] + [Route("apps/restore/")] + [ProducesResponseType(typeof(RestoreJobDto), 200)] + [ApiPermission(Permissions.AdminRestore)] + public async Task GetRestoreJob() + { + var restoreGrain = grainFactory.GetGrain(SingleGrain.Id); + + var job = await restoreGrain.GetJobAsync(); + + if (job.Value == null) + { + return NotFound(); + } + + var response = RestoreJobDto.FromJob(job.Value); + + return Ok(response); + } + + /// + /// Restore a backup. + /// + /// The backup to restore. + /// + /// 204 => Restore operation started. + /// + [HttpPost] + [Route("apps/restore/")] + [ApiPermission(Permissions.AdminRestore)] + public async Task PostRestoreJob([FromBody] RestoreRequestDto request) + { + var restoreGrain = grainFactory.GetGrain(SingleGrain.Id); + + await restoreGrain.RestoreAsync(request.Url, User.Token()!, request.Name); + + return NoContent(); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs new file mode 100644 index 000000000..9f2bb9283 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -0,0 +1,457 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using Squidex.Areas.Api.Controllers.Contents.Models; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Infrastructure.Commands; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Contents +{ + public sealed class ContentsController : ApiController + { + private readonly MyContentsControllerOptions controllerOptions; + private readonly IContentQueryService contentQuery; + private readonly IContentWorkflow contentWorkflow; + private readonly IGraphQLService graphQl; + + public ContentsController(ICommandBus commandBus, + IContentQueryService contentQuery, + IContentWorkflow contentWorkflow, + IGraphQLService graphQl, + IOptions controllerOptions) + : base(commandBus) + { + this.contentQuery = contentQuery; + this.contentWorkflow = contentWorkflow; + this.controllerOptions = controllerOptions.Value; + + this.graphQl = graphQl; + } + + /// + /// GraphQL endpoint. + /// + /// The name of the app. + /// The graphql query. + /// + /// 200 => Contents retrieved or mutated. + /// 404 => Schema or app not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [HttpPost] + [Route("content/{app}/graphql/")] + [ApiPermission] + [ApiCosts(2)] + public async Task PostGraphQL(string app, [FromBody] GraphQLQuery query) + { + var (hasError, response) = await graphQl.QueryAsync(Context, query); + + if (hasError) + { + return BadRequest(response); + } + else + { + return Ok(response); + } + } + + /// + /// GraphQL endpoint (Batch). + /// + /// The name of the app. + /// The graphql queries. + /// + /// 200 => Contents retrieved or mutated. + /// 404 => Schema or app not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [HttpPost] + [Route("content/{app}/graphql/batch")] + [ApiPermission] + [ApiCosts(2)] + public async Task PostGraphQLBatch(string app, [FromBody] GraphQLQuery[] batch) + { + var (hasError, response) = await graphQl.QueryAsync(Context, batch); + + if (hasError) + { + return BadRequest(response); + } + else + { + return Ok(response); + } + } + + /// + /// Queries contents. + /// + /// The name of the app. + /// The optional ids of the content to fetch. + /// + /// 200 => Contents retrieved. + /// 404 => App not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [Route("content/{app}/")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermission] + [ApiCosts(1)] + public async Task GetAllContents(string app, [FromQuery] string ids) + { + var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(ids).Ids); + + var response = Deferred.AsyncResponse(() => + { + return ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow); + }); + + if (ShouldProvideSurrogateKeys(contents)) + { + Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys(); + } + + Response.Headers[HeaderNames.ETag] = contents.ToEtag(); + + return Ok(response); + } + + /// + /// Queries contents. + /// + /// The name of the app. + /// The name of the schema. + /// The optional ids of the content to fetch. + /// The optional json query. + /// + /// 200 => Contents retrieved. + /// 404 => Schema or app not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [Route("content/{app}/{name}/")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermission] + [ApiCosts(1)] + public async Task GetContents(string app, string name, [FromQuery] string? ids = null, [FromQuery] string? q = null) + { + var schema = await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var contents = await contentQuery.QueryAsync(Context, name, + Q.Empty + .WithIds(ids) + .WithJsonQuery(q) + .WithODataQuery(Request.QueryString.ToString())); + + var response = Deferred.AsyncResponse(async () => + { + return await ContentsDto.FromContentsAsync(contents, Context, this, schema, contentWorkflow); + }); + + if (ShouldProvideSurrogateKeys(contents)) + { + Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys(); + } + + Response.Headers[HeaderNames.ETag] = contents.ToEtag(); + + return Ok(response); + } + + /// + /// Get a content item. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content to fetch. + /// + /// 200 => Content found. + /// 404 => Content, schema or app not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [Route("content/{app}/{name}/{id}/")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermission] + [ApiCosts(1)] + public async Task GetContent(string app, string name, Guid id) + { + var content = await contentQuery.FindContentAsync(Context, name, id); + + var response = ContentDto.FromContent(Context, content, this); + + if (controllerOptions.EnableSurrogateKeys) + { + Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); + } + + Response.Headers[HeaderNames.ETag] = content.ToEtag(); + + return Ok(response); + } + + /// + /// Get a content by version. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content to fetch. + /// The version fo the content to fetch. + /// + /// 200 => Content found. + /// 404 => Content, schema or app not found. + /// 400 => Content data is not valid. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [Route("content/{app}/{name}/{id}/{version}/")] + [ApiPermission(Permissions.AppContentsRead)] + [ApiCosts(1)] + public async Task GetContentVersion(string app, string name, Guid id, int version) + { + var content = await contentQuery.FindContentAsync(Context, name, id, version); + + var response = ContentDto.FromContent(Context, content, this); + + if (controllerOptions.EnableSurrogateKeys) + { + Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); + } + + Response.Headers[HeaderNames.ETag] = content.ToEtag(); + + return Ok(response.Data); + } + + /// + /// Create a content item. + /// + /// The name of the app. + /// The name of the schema. + /// The full data for the content item. + /// Indicates whether the content should be published immediately. + /// + /// 201 => Content created. + /// 404 => Content, schema or app not found. + /// 400 => Content data is not valid. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpPost] + [Route("content/{app}/{name}/")] + [ProducesResponseType(typeof(ContentsDto), 201)] + [ApiPermission(Permissions.AppContentsCreate)] + [ApiCosts(1)] + public async Task PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false) + { + await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish }; + + var response = await InvokeCommandAsync(command); + + return CreatedAtAction(nameof(GetContent), new { app, name, id = command.ContentId }, response); + } + + /// + /// Update a content item. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content item to update. + /// The full data for the content item. + /// Indicates whether the update is a proposal. + /// + /// 200 => Content updated. + /// 404 => Content, schema or app not found. + /// 400 => Content data is not valid. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpPut] + [Route("content/{app}/{name}/{id}/")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermission(Permissions.AppContentsUpdate)] + [ApiCosts(1)] + public async Task PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) + { + await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var command = new UpdateContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// + /// Patchs a content item. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content item to patch. + /// The patch for the content item. + /// Indicates whether the patch is a proposal. + /// + /// 200 => Content patched. + /// 404 => Content, schema or app not found. + /// 400 => Content patch is not valid. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpPatch] + [Route("content/{app}/{name}/{id}/")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermission(Permissions.AppContentsUpdate)] + [ApiCosts(1)] + public async Task PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) + { + await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var command = new PatchContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// + /// Publish a content item. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content item to publish. + /// The status request. + /// + /// 200 => Content published. + /// 404 => Content, schema or app not found. + /// 400 => Request is not valid. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpPut] + [Route("content/{app}/{name}/{id}/status/")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermission] + [ApiCosts(1)] + public async Task PutContentStatus(string app, string name, Guid id, ChangeStatusDto request) + { + await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var command = request.ToCommand(id); + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// + /// Discard changes. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content item to discard changes. + /// + /// 200 => Content restored. + /// 404 => Content, schema or app not found. + /// 400 => Content was not archived. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpPut] + [Route("content/{app}/{name}/{id}/discard/")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermission(Permissions.AppContentsDraftDiscard)] + [ApiCosts(1)] + public async Task DiscardDraft(string app, string name, Guid id) + { + await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var command = new DiscardChanges { ContentId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// + /// Delete a content item. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content item to delete. + /// + /// 204 => Content deleted. + /// 404 => Content, schema or app not found. + /// + /// + /// You can create an generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpDelete] + [Route("content/{app}/{name}/{id}/")] + [ApiPermission(Permissions.AppContentsDelete)] + [ApiCosts(1)] + public async Task DeleteContent(string app, string name, Guid id) + { + await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var command = new DeleteContent { ContentId = id }; + + await CommandBus.PublishAsync(command); + + return NoContent(); + } + + private async Task InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command); + + var result = context.Result(); + var response = ContentDto.FromContent(Context, result, this); + + return response; + } + + private bool ShouldProvideSurrogateKeys(IReadOnlyList response) + { + return controllerOptions.EnableSurrogateKeys && response.Count <= controllerOptions.MaxItemsForSurrogateKeys; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs new file mode 100644 index 000000000..0e415924e --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -0,0 +1,194 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using NodaTime; +using Squidex.Areas.Api.Controllers.Schemas.Models; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Contents.Models +{ + public sealed class ContentDto : Resource + { + /// + /// The if of the content item. + /// + public Guid Id { get; set; } + + /// + /// The user that has created the content item. + /// + [Required] + public RefToken CreatedBy { get; set; } + + /// + /// The user that has updated the content item. + /// + [Required] + public RefToken LastModifiedBy { get; set; } + + /// + /// The data of the content item. + /// + [Required] + public object? Data { get; set; } + + /// + /// The pending changes of the content item. + /// + public object? DataDraft { get; set; } + + /// + /// The reference data for the frontend UI. + /// + public NamedContentData? ReferenceData { get; set; } + + /// + /// Indicates if the draft data is pending. + /// + public bool IsPending { get; set; } + + /// + /// The scheduled status. + /// + public ScheduleJobDto? ScheduleJob { get; set; } + + /// + /// The date and time when the content item has been created. + /// + public Instant Created { get; set; } + + /// + /// The date and time when the content item has been modified last. + /// + public Instant LastModified { get; set; } + + /// + /// The status of the content. + /// + public Status Status { get; set; } + + /// + /// The color of the status. + /// + public string StatusColor { get; set; } + + /// + /// The name of the schema. + /// + public string SchemaName { get; set; } + + /// + /// The display name of the schema. + /// + public string SchemaDisplayName { get; set; } + + /// + /// The reference fields. + /// + public FieldDto[] ReferenceFields { get; set; } + + /// + /// The version of the content. + /// + public long Version { get; set; } + + public static ContentDto FromContent(Context context, IEnrichedContentEntity content, ApiController controller) + { + var response = SimpleMapper.Map(content, new ContentDto()); + + if (context.IsFlatten()) + { + response.Data = content.Data?.ToFlatten(); + response.DataDraft = content.DataDraft?.ToFlatten(); + } + else + { + response.Data = content.Data; + response.DataDraft = content.DataDraft; + } + + if (content.ReferenceFields != null) + { + response.ReferenceFields = content.ReferenceFields.Select(FieldDto.FromField).ToArray(); + } + + if (content.ScheduleJob != null) + { + response.ScheduleJob = SimpleMapper.Map(content.ScheduleJob, new ScheduleJobDto()); + } + + return response.CreateLinksAsync(content, controller, content.AppId.Name, content.SchemaId.Name); + } + + private ContentDto CreateLinksAsync(IEnrichedContentEntity content, ApiController controller, string app, string schema) + { + var values = new { app, name = schema, id = Id }; + + AddSelfLink(controller.Url(x => nameof(x.GetContent), values)); + + if (Version > 0) + { + var versioned = new { app, name = schema, id = Id, version = Version - 1 }; + + AddGetLink("prev", controller.Url(x => nameof(x.GetContentVersion), versioned)); + } + + if (IsPending) + { + if (controller.HasPermission(Permissions.AppContentsDraftDiscard, app, schema)) + { + AddPutLink("draft/discard", controller.Url(x => nameof(x.DiscardDraft), values)); + } + + if (controller.HasPermission(Permissions.AppContentsDraftPublish, app, schema)) + { + AddPutLink("draft/publish", controller.Url(x => nameof(x.PutContentStatus), values)); + } + } + + if (controller.HasPermission(Permissions.AppContentsUpdate, app, schema)) + { + if (content.CanUpdate) + { + AddPutLink("update", controller.Url(x => nameof(x.PutContent), values)); + } + + if (Status == Status.Published) + { + AddPutLink("draft/propose", controller.Url(x => nameof(x.PutContent), values) + "?asDraft=true"); + } + + AddPatchLink("patch", controller.Url(x => nameof(x.PatchContent), values)); + + if (content.Nexts != null) + { + foreach (var next in content.Nexts) + { + AddPutLink($"status/{next.Status}", controller.Url(x => nameof(x.PutContentStatus), values), next.Color); + } + } + } + + if (controller.HasPermission(Permissions.AppContentsDelete, app, schema)) + { + AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteContent), values)); + } + + return this; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs new file mode 100644 index 000000000..c6916f9c2 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -0,0 +1,81 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Contents.Models +{ + public sealed class ContentsDto : Resource + { + /// + /// The total number of content items. + /// + public long Total { get; set; } + + /// + /// The content items. + /// + [Required] + public ContentDto[] Items { get; set; } + + /// + /// The possible statuses. + /// + [Required] + public StatusInfoDto[] Statuses { get; set; } + + public static async Task FromContentsAsync(IResultList contents, Context context, ApiController controller, + ISchemaEntity? schema, IContentWorkflow workflow) + { + var result = new ContentsDto + { + Total = contents.Total, + Items = contents.Select(x => ContentDto.FromContent(context, x, controller)).ToArray() + }; + + if (schema != null) + { + await result.AssignStatusesAsync(workflow, schema); + + result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name); + } + + return result; + } + + private async Task AssignStatusesAsync(IContentWorkflow workflow, ISchemaEntity schema) + { + var allStatuses = await workflow.GetAllAsync(schema); + + Statuses = allStatuses.Select(StatusInfoDto.FromStatusInfo).ToArray(); + } + + private ContentsDto CreateLinks(ApiController controller, string app, string schema) + { + var values = new { app, name = schema }; + + AddSelfLink(controller.Url(x => nameof(x.GetContents), values)); + + if (controller.HasPermission(Permissions.AppContentsCreate, app, schema)) + { + AddPostLink("create", controller.Url(x => nameof(x.PostContent), values)); + + AddPostLink("create/publish", controller.Url(x => nameof(x.PostContent), values) + "?publish=true"); + } + + return this; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs diff --git a/src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/DocsVM.cs b/backend/src/Squidex/Areas/Api/Controllers/DocsVM.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/DocsVM.cs rename to backend/src/Squidex/Areas/Api/Controllers/DocsVM.cs diff --git a/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs rename to backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs diff --git a/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/History/HistoryController.cs rename to backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs diff --git a/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs b/backend/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/LanguageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/LanguageDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/LanguageDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/LanguageDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs diff --git a/src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs b/backend/src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/News/Models/FeaturesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/News/Models/FeaturesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/News/Models/FeaturesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/News/Models/FeaturesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs b/backend/src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs rename to backend/src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs diff --git a/src/Squidex/Areas/Api/Controllers/News/NewsController.cs b/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/News/NewsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs b/backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs rename to backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs diff --git a/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs b/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Ping/PingController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs new file mode 100644 index 000000000..02be95cbe --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -0,0 +1,93 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using Squidex.Areas.Api.Controllers.Plans.Models; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure.Commands; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Plans +{ + /// + /// Manages and configures plans. + /// + [ApiExplorerSettings(GroupName = nameof(Plans))] + public sealed class AppPlansController : ApiController + { + private readonly IAppPlansProvider appPlansProvider; + private readonly IAppPlanBillingManager appPlansBillingManager; + + public AppPlansController(ICommandBus commandBus, + IAppPlansProvider appPlansProvider, + IAppPlanBillingManager appPlansBillingManager) + : base(commandBus) + { + this.appPlansProvider = appPlansProvider; + this.appPlansBillingManager = appPlansBillingManager; + } + + /// + /// Get app plan information. + /// + /// The name of the app. + /// + /// 200 => App plan information returned. + /// 404 => App not found. + /// + [HttpGet] + [Route("apps/{app}/plans/")] + [ProducesResponseType(typeof(AppPlansDto), 200)] + [ApiPermission(Permissions.AppPlansRead)] + [ApiCosts(0)] + public IActionResult GetPlans(string app) + { + var hasPortal = appPlansBillingManager.HasPortal; + + var response = Deferred.Response(() => + { + return AppPlansDto.FromApp(App, appPlansProvider, hasPortal); + }); + + Response.Headers[HeaderNames.ETag] = App.ToEtag(); + + return Ok(response); + } + + /// + /// Change the app plan. + /// + /// The name of the app. + /// Plan object that needs to be changed. + /// + /// 200 => Plan changed or redirect url returned. + /// 400 => Plan not owned by user. + /// 404 => App not found. + /// + [HttpPut] + [Route("apps/{app}/plan/")] + [ProducesResponseType(typeof(PlanChangedDto), 200)] + [ApiPermission(Permissions.AppPlansChange)] + [ApiCosts(0)] + public async Task PutPlan(string app, [FromBody] ChangePlanDto request) + { + var context = await CommandBus.PublishAsync(request.ToCommand()); + + string? redirectUri = null; + + if (context.PlainResult is RedirectToCheckoutResult result) + { + redirectUri = result.Url.ToString(); + } + + return Ok(new PlanChangedDto { RedirectUri = redirectUri }); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs new file mode 100644 index 000000000..25a24be1e --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Services; + +namespace Squidex.Areas.Api.Controllers.Plans.Models +{ + public sealed class AppPlansDto + { + /// + /// The available plans. + /// + [Required] + public PlanDto[] Plans { get; set; } + + /// + /// The current plan id. + /// + public string? CurrentPlanId { get; set; } + + /// + /// The plan owner. + /// + public string? PlanOwner { get; set; } + + /// + /// Indicates if there is a billing portal. + /// + public bool HasPortal { get; set; } + + public static AppPlansDto FromApp(IAppEntity app, IAppPlansProvider plans, bool hasPortal) + { + var planId = app.Plan?.PlanId; + + var response = new AppPlansDto + { + CurrentPlanId = planId, + Plans = plans.GetAvailablePlans().Select(PlanDto.FromPlan).ToArray(), + PlanOwner = app.Plan?.Owner.Identifier, + HasPortal = hasPortal + }; + + return response; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs new file mode 100644 index 000000000..158a6cd96 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Areas.Api.Controllers.Plans.Models +{ + public sealed class PlanChangedDto + { + /// + /// Optional redirect uri. + /// + public string? RedirectUri { get; set; } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs new file mode 100644 index 000000000..f2494c5e1 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Namotion.Reflection; +using NJsonSchema; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure; + +namespace Squidex.Areas.Api.Controllers.Rules.Models +{ + public sealed class RuleActionProcessor : IDocumentProcessor + { + private readonly RuleRegistry ruleRegistry; + + public RuleActionProcessor(RuleRegistry ruleRegistry) + { + Guard.NotNull(ruleRegistry); + + this.ruleRegistry = ruleRegistry; + } + + public void Process(DocumentProcessorContext context) + { + try + { + var schema = context.SchemaResolver.GetSchema(typeof(RuleAction), false); + + if (schema != null) + { + schema.DiscriminatorObject = new OpenApiDiscriminator + { + JsonInheritanceConverter = new RuleActionConverter(), PropertyName = "actionType" + }; + + schema.Properties["actionType"] = new JsonSchemaProperty + { + Type = JsonObjectType.String, IsRequired = true + }; + + foreach (var (key, value) in ruleRegistry.Actions) + { + var derivedSchema = context.SchemaGenerator.Generate(value.Type.ToContextualType(), context.SchemaResolver); + + var oldName = context.Document.Definitions.FirstOrDefault(x => x.Value == derivedSchema).Key; + + if (oldName != null) + { + context.Document.Definitions.Remove(oldName); + context.Document.Definitions.Add($"{key}RuleActionDto", derivedSchema); + } + } + + RemoveFreezable(context, schema); + } + } + catch (KeyNotFoundException) + { + return; + } + } + + private static void RemoveFreezable(DocumentProcessorContext context, JsonSchema schema) + { + context.Document.Definitions.Remove("Freezable"); + + schema.AllOf.Clear(); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/ChangeCategoryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ChangeCategoryDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/ChangeCategoryDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ChangeCategoryDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs new file mode 100644 index 000000000..0c127730b --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Web.Json; + +namespace Squidex.Areas.Api.Controllers.Schemas.Models +{ + [JsonConverter(typeof(TypedJsonInheritanceConverter), "fieldType")] + [KnownType(nameof(Subtypes))] + public abstract class FieldPropertiesDto + { + /// + /// Optional label for the editor. + /// + [StringLength(100)] + public string? Label { get; set; } + + /// + /// Hints to describe the schema. + /// + [StringLength(1000)] + public string? Hints { get; set; } + + /// + /// Placeholder to show when no value has been entered. + /// + [StringLength(100)] + public string? Placeholder { get; set; } + + /// + /// Indicates if the field is required. + /// + public bool IsRequired { get; set; } + + /// + /// Determines if the field should be displayed in lists. + /// + public bool IsListField { get; set; } + + /// + /// Determines if the field should be displayed in reference lists. + /// + public bool IsReferenceField { get; set; } + + /// + /// Optional url to the editor. + /// + public string? EditorUrl { get; set; } + + /// + /// Tags for automation processes. + /// + public ReadOnlyCollection Tags { get; set; } + + public abstract FieldProperties ToProperties(); + + public static Type[] Subtypes() + { + var type = typeof(FieldPropertiesDto); + + return type.Assembly.GetTypes().Where(type.IsAssignableFrom).ToArray(); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs new file mode 100644 index 000000000..173b9271a --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Entities.Schemas.Commands; + +namespace Squidex.Areas.Api.Controllers.Schemas.Models +{ + public sealed class UpdateFieldDto + { + /// + /// The field properties. + /// + [Required] + public FieldPropertiesDto Properties { get; set; } + + public UpdateField ToCommand(long id, long? parentId = null) + { + return new UpdateField { ParentFieldId = parentId, FieldId = id, Properties = Properties?.ToProperties()! }; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs new file mode 100644 index 000000000..be7290131 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Schemas.Models +{ + public abstract class UpsertSchemaDto + { + /// + /// The optional properties. + /// + public SchemaPropertiesDto? Properties { get; set; } + + /// + /// The optional scripts. + /// + public SchemaScriptsDto? Scripts { get; set; } + + /// + /// Optional fields. + /// + public List Fields { get; set; } + + /// + /// The optional preview urls. + /// + public Dictionary? PreviewUrls { get; set; } + + /// + /// The category. + /// + public string Category { get; set; } + + /// + /// Set it to true to autopublish the schema. + /// + public bool IsPublished { get; set; } + + public static TCommand ToCommand(TDto dto, TCommand command) where TCommand : UpsertCommand where TDto : UpsertSchemaDto + { + SimpleMapper.Map(dto, command); + + if (dto.Properties != null) + { + command.Properties = new SchemaProperties(); + + SimpleMapper.Map(dto.Properties, command.Properties); + } + + if (dto.Scripts != null) + { + command.Scripts = new SchemaScripts(); + + SimpleMapper.Map(dto.Scripts, command.Scripts); + } + + if (dto.Fields != null) + { + command.Fields = new List(); + + foreach (var rootFieldDto in dto.Fields) + { + var rootProperties = rootFieldDto?.Properties?.ToProperties(); + var rootField = new UpsertSchemaField { Properties = rootProperties! }; + + if (rootFieldDto != null) + { + SimpleMapper.Map(rootFieldDto, rootField); + + if (rootFieldDto?.Nested?.Count > 0) + { + rootField.Nested = new List(); + + foreach (var nestedFieldDto in rootFieldDto.Nested) + { + var nestedProperties = nestedFieldDto?.Properties?.ToProperties(); + var nestedField = new UpsertSchemaNestedField { Properties = nestedProperties! }; + + if (nestedFieldDto != null) + { + SimpleMapper.Map(nestedFieldDto, nestedField); + } + + rootField.Nested.Add(nestedField); + } + } + } + + command.Fields.Add(rootField); + } + } + + return command; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs new file mode 100644 index 000000000..14154d376 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -0,0 +1,330 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using Squidex.Areas.Api.Controllers.Schemas.Models; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure.Commands; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Schemas +{ + /// + /// Manages and retrieves information about schemas. + /// + [ApiExplorerSettings(GroupName = nameof(Schemas))] + public sealed class SchemasController : ApiController + { + private readonly IAppProvider appProvider; + + public SchemasController(ICommandBus commandBus, IAppProvider appProvider) + : base(commandBus) + { + this.appProvider = appProvider; + } + + /// + /// Get schemas. + /// + /// The name of the app. + /// + /// 200 => Schemas returned. + /// 404 => App not found. + /// + [HttpGet] + [Route("apps/{app}/schemas/")] + [ProducesResponseType(typeof(SchemasDto), 200)] + [ApiPermission(Permissions.AppCommon)] + [ApiCosts(0)] + public async Task GetSchemas(string app) + { + var schemas = await appProvider.GetSchemasAsync(AppId); + + var response = Deferred.Response(() => + { + return SchemasDto.FromSchemas(schemas, this, app); + }); + + Response.Headers[HeaderNames.ETag] = schemas.ToEtag(); + + return Ok(response); + } + + /// + /// Get a schema by name. + /// + /// The name of the app. + /// The name of the schema to retrieve. + /// + /// 200 => Schema found. + /// 404 => Schema or app not found. + /// + [HttpGet] + [Route("apps/{app}/schemas/{name}/")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppCommon)] + [ApiCosts(0)] + public async Task GetSchema(string app, string name) + { + ISchemaEntity? schema; + + if (Guid.TryParse(name, out var id)) + { + schema = await appProvider.GetSchemaAsync(AppId, id); + } + else + { + schema = await appProvider.GetSchemaAsync(AppId, name); + } + + if (schema == null || schema.IsDeleted) + { + return NotFound(); + } + + var response = Deferred.Response(() => + { + return SchemaDetailsDto.FromSchemaWithDetails(schema, this, app); + }); + + Response.Headers[HeaderNames.ETag] = schema.ToEtag(); + + return Ok(response); + } + + /// + /// Create a new schema. + /// + /// The name of the app. + /// The schema object that needs to be added to the app. + /// + /// 201 => Schema created. + /// 400 => Schema name or properties are not valid. + /// 409 => Schema name already in use. + /// + [HttpPost] + [Route("apps/{app}/schemas/")] + [ProducesResponseType(typeof(SchemaDetailsDto), 201)] + [ApiPermission(Permissions.AppSchemasCreate)] + [ApiCosts(1)] + public async Task PostSchema(string app, [FromBody] CreateSchemaDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(app, command); + + return CreatedAtAction(nameof(GetSchema), new { app, name = request.Name }, response); + } + + /// + /// Update a schema. + /// + /// The name of the app. + /// The name of the schema. + /// The schema object that needs to updated. + /// + /// 200 => Schema updated. + /// 400 => Schema properties are not valid. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task PutSchema(string app, string name, [FromBody] UpdateSchemaDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Synchronize a schema. + /// + /// The name of the app. + /// The name of the schema. + /// The schema object that needs to updated. + /// + /// 200 => Schema updated. + /// 400 => Schema properties are not valid. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/sync")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task PutSchemaSync(string app, string name, [FromBody] SynchronizeSchemaDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Update a schema category. + /// + /// The name of the app. + /// The name of the schema. + /// The schema object that needs to updated. + /// + /// 200 => Schema updated. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/category")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task PutCategory(string app, string name, [FromBody] ChangeCategoryDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Update the preview urls. + /// + /// The name of the app. + /// The name of the schema. + /// The preview urls for the schema. + /// + /// 200 => Schema updated. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/preview-urls")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task PutPreviewUrls(string app, string name, [FromBody] ConfigurePreviewUrlsDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Update the scripts. + /// + /// The name of the app. + /// The name of the schema. + /// The schema scripts object that needs to updated. + /// + /// 200 => Schema updated. + /// 400 => Schema properties are not valid. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/scripts/")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppSchemasScripts)] + [ApiCosts(1)] + public async Task PutScripts(string app, string name, [FromBody] SchemaScriptsDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Publish a schema. + /// + /// The name of the app. + /// The name of the schema to publish. + /// + /// 200 => Schema has been published. + /// 400 => Schema is already published. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/publish/")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppSchemasPublish)] + [ApiCosts(1)] + public async Task PublishSchema(string app, string name) + { + var command = new PublishSchema(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Unpublish a schema. + /// + /// The name of the app. + /// The name of the schema to unpublish. + /// + /// 200 => Schema has been unpublished. + /// 400 => Schema is not published. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/unpublish/")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppSchemasPublish)] + [ApiCosts(1)] + public async Task UnpublishSchema(string app, string name) + { + var command = new UnpublishSchema(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Delete a schema. + /// + /// The name of the app. + /// The name of the schema to delete. + /// + /// 204 => Schema deleted. + /// 404 => Schema or app not found. + /// + [HttpDelete] + [Route("apps/{app}/schemas/{name}/")] + [ApiPermission(Permissions.AppSchemasDelete)] + [ApiCosts(1)] + public async Task DeleteSchema(string app, string name) + { + await CommandBus.PublishAsync(new DeleteSchema()); + + return NoContent(); + } + + private async Task InvokeCommandAsync(string app, ICommand command) + { + var context = await CommandBus.PublishAsync(command); + + var result = context.Result(); + var response = SchemaDetailsDto.FromSchemaWithDetails(result, this, app); + + return response; + } + } +} \ No newline at end of file diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentCallsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentCallsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentCallsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentCallsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/Models/LogDownloadDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/LogDownloadDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Statistics/Models/LogDownloadDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/LogDownloadDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/UI/Models/UpdateSettingDto.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UpdateSettingDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/UI/Models/UpdateSettingDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/UI/Models/UpdateSettingDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs rename to backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs diff --git a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/UI/UIController.cs rename to backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Users/Assets/Avatar.png b/backend/src/Squidex/Areas/Api/Controllers/Users/Assets/Avatar.png similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Users/Assets/Avatar.png rename to backend/src/Squidex/Areas/Api/Controllers/Users/Assets/Avatar.png diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs new file mode 100644 index 000000000..dbc08c87d --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs @@ -0,0 +1,95 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared.Users; +using Squidex.Web; +using AllPermissions = Squidex.Shared.Permissions; + +namespace Squidex.Areas.Api.Controllers.Users.Models +{ + public sealed class UserDto : Resource + { + /// + /// The id of the user. + /// + [Required] + public string Id { get; set; } + + /// + /// The email of the user. Unique value. + /// + [Required] + public string Email { get; set; } + + /// + /// The display name (usually first name and last name) of the user. + /// + [Required] + public string DisplayName { get; set; } + + /// + /// Determines if the user is locked. + /// + [Required] + public bool IsLocked { get; set; } + + /// + /// Additional permissions for the user. + /// + [Required] + public IEnumerable Permissions { get; set; } + + public static UserDto FromUser(IUser user, ApiController controller) + { + var userPermssions = user.Permissions().ToIds(); + var userName = user.DisplayName()!; + + var result = SimpleMapper.Map(user, new UserDto { DisplayName = userName, Permissions = userPermssions }); + + return result.CreateLinks(controller); + } + + private UserDto CreateLinks(ApiController controller) + { + var values = new { id = Id }; + + if (controller is UserManagementController) + { + AddSelfLink(controller.Url(c => nameof(c.GetUser), values)); + } + else + { + AddSelfLink(controller.Url(c => nameof(c.GetUser), values)); + } + + if (!controller.IsUser(Id)) + { + if (controller.HasPermission(AllPermissions.AdminUsersLock) && !IsLocked) + { + AddPutLink("lock", controller.Url(c => nameof(c.LockUser), values)); + } + + if (controller.HasPermission(AllPermissions.AdminUsersUnlock) && IsLocked) + { + AddPutLink("unlock", controller.Url(c => nameof(c.UnlockUser), values)); + } + } + + if (controller.HasPermission(AllPermissions.AdminUsersUpdate)) + { + AddPutLink("update", controller.Url(c => nameof(c.PutUser), values)); + } + + AddGetLink("picture", controller.Url(c => nameof(c.GetUserPicture), values)); + + return this; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs new file mode 100644 index 000000000..20638bb74 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs @@ -0,0 +1,129 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Squidex.Areas.Api.Controllers.Users.Models; +using Squidex.Domain.Users; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Validation; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Users +{ + [ApiModelValidation(true)] + public sealed class UserManagementController : ApiController + { + private readonly UserManager userManager; + private readonly IUserFactory userFactory; + + public UserManagementController(ICommandBus commandBus, UserManager userManager, IUserFactory userFactory) + : base(commandBus) + { + this.userManager = userManager; + this.userFactory = userFactory; + } + + [HttpGet] + [Route("user-management/")] + [ProducesResponseType(typeof(UsersDto), 200)] + [ApiPermission(Permissions.AdminUsersRead)] + public async Task GetUsers([FromQuery] string? query = null, [FromQuery] int skip = 0, [FromQuery] int take = 10) + { + var taskForItems = userManager.QueryByEmailAsync(query, take, skip); + var taskForCount = userManager.CountByEmailAsync(query); + + await Task.WhenAll(taskForItems, taskForCount); + + var response = UsersDto.FromResults(taskForItems.Result, taskForCount.Result, this); + + return Ok(response); + } + + [HttpGet] + [Route("user-management/{id}/")] + [ProducesResponseType(typeof(UserDto), 201)] + [ApiPermission(Permissions.AdminUsersRead)] + public async Task GetUser(string id) + { + var user = await userManager.FindByIdWithClaimsAsync(id); + + if (user == null) + { + return NotFound(); + } + + var response = UserDto.FromUser(user, this); + + return Ok(response); + } + + [HttpPost] + [Route("user-management/")] + [ProducesResponseType(typeof(UserDto), 201)] + [ApiPermission(Permissions.AdminUsersCreate)] + public async Task PostUser([FromBody] CreateUserDto request) + { + var user = await userManager.CreateAsync(userFactory, request.ToValues()); + + var response = UserDto.FromUser(user, this); + + return Ok(response); + } + + [HttpPut] + [Route("user-management/{id}/")] + [ProducesResponseType(typeof(UserDto), 201)] + [ApiPermission(Permissions.AdminUsersUpdate)] + public async Task PutUser(string id, [FromBody] UpdateUserDto request) + { + var user = await userManager.UpdateAsync(id, request.ToValues()); + + var response = UserDto.FromUser(user, this); + + return Ok(response); + } + + [HttpPut] + [Route("user-management/{id}/lock/")] + [ProducesResponseType(typeof(UserDto), 201)] + [ApiPermission(Permissions.AdminUsersLock)] + public async Task LockUser(string id) + { + if (this.IsUser(id)) + { + throw new ValidationException("Locking user failed.", new ValidationError("You cannot lock yourself.")); + } + + var user = await userManager.LockAsync(id); + + var response = UserDto.FromUser(user, this); + + return Ok(response); + } + + [HttpPut] + [Route("user-management/{id}/unlock/")] + [ProducesResponseType(typeof(UserDto), 201)] + [ApiPermission(Permissions.AdminUsersUnlock)] + public async Task UnlockUser(string id) + { + if (this.IsUser(id)) + { + throw new ValidationException("Unlocking user failed.", new ValidationError("You cannot unlock yourself.")); + } + + var user = await userManager.UnlockAsync(id); + + var response = UserDto.FromUser(user, this); + + return Ok(response); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs new file mode 100644 index 000000000..64ab97444 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs @@ -0,0 +1,198 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Squidex.Areas.Api.Controllers.Users.Models; +using Squidex.Domain.Users; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Shared.Users; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Users +{ + /// + /// Readonly API to retrieve information about squidex users. + /// + [ApiExplorerSettings(GroupName = nameof(Users))] + public sealed class UsersController : ApiController + { + private static readonly byte[] AvatarBytes; + private readonly IUserPictureStore userPictureStore; + private readonly IUserResolver userResolver; + private readonly ISemanticLog log; + + static UsersController() + { + var assembly = typeof(UsersController).Assembly; + + using (var avatarStream = assembly.GetManifestResourceStream("Squidex.Areas.Api.Controllers.Users.Assets.Avatar.png")) + { + AvatarBytes = new byte[avatarStream!.Length]; + + avatarStream.Read(AvatarBytes, 0, AvatarBytes.Length); + } + } + + public UsersController( + ICommandBus commandBus, + IUserPictureStore userPictureStore, + IUserResolver userResolver, + ISemanticLog log) + : base(commandBus) + { + this.userPictureStore = userPictureStore; + this.userResolver = userResolver; + + this.log = log; + } + + /// + /// Get the user resources. + /// + /// + /// 200 => User resources returned. + /// + [HttpGet] + [Route("/")] + [ProducesResponseType(typeof(ResourcesDto), 200)] + [ApiPermission] + public IActionResult GetUserResources() + { + var response = ResourcesDto.FromController(this); + + return Ok(response); + } + + /// + /// Get users by query. + /// + /// The query to search the user by email address. Case invariant. + /// + /// Search the user by query that contains the email address or the part of the email address. + /// + /// + /// 200 => Users returned. + /// + [HttpGet] + [Route("users/")] + [ProducesResponseType(typeof(UserDto[]), 200)] + [ApiPermission] + public async Task GetUsers(string query) + { + try + { + var users = await userResolver.QueryByEmailAsync(query); + + var response = users.Where(x => !x.IsHidden()).Select(x => UserDto.FromUser(x, this)).ToArray(); + + return Ok(response); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", nameof(GetUsers)) + .WriteProperty("status", "Failed")); + } + + return Ok(new UserDto[0]); + } + + /// + /// Get user by id. + /// + /// The id of the user (GUID). + /// + /// 200 => User found. + /// 404 => User not found. + /// + [HttpGet] + [Route("users/{id}/")] + [ProducesResponseType(typeof(UserDto), 200)] + [ApiPermission] + public async Task GetUser(string id) + { + try + { + var entity = await userResolver.FindByIdOrEmailAsync(id); + + if (entity != null) + { + var response = UserDto.FromUser(entity, this); + + return Ok(response); + } + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", nameof(GetUser)) + .WriteProperty("status", "Failed")); + } + + return NotFound(); + } + + /// + /// Get user picture by id. + /// + /// The id of the user (GUID). + /// + /// 200 => User found and image or fallback returned. + /// 404 => User not found. + /// + [HttpGet] + [Route("users/{id}/picture/")] + [ProducesResponseType(typeof(FileResult), 200)] + [ResponseCache(Duration = 300)] + public async Task GetUserPicture(string id) + { + try + { + var entity = await userResolver.FindByIdOrEmailAsync(id); + + if (entity != null) + { + if (entity.IsPictureUrlStored()) + { + return new FileStreamResult(await userPictureStore.DownloadAsync(entity.Id), "image/png"); + } + + using (var client = new HttpClient()) + { + var url = entity.PictureNormalizedUrl(); + + if (!string.IsNullOrWhiteSpace(url)) + { + var response = await client.GetAsync(url); + + if (response.IsSuccessStatusCode) + { + var contentType = response.Content.Headers.ContentType.ToString(); + + return new FileStreamResult(await response.Content.ReadAsStreamAsync(), contentType); + } + } + } + } + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", nameof(GetUser)) + .WriteProperty("status", "Failed")); + } + + return new FileStreamResult(new MemoryStream(AvatarBytes), "image/png"); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Startup.cs b/backend/src/Squidex/Areas/Api/Startup.cs new file mode 100644 index 000000000..e8fab3e7b --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Startup.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Squidex.Areas.Api.Config.OpenApi; +using Squidex.Web; + +namespace Squidex.Areas.Api +{ + public static class Startup + { + public static void ConfigureApi(this IApplicationBuilder app) + { + app.Map(Constants.ApiPrefix, appApi => + { + appApi.UseRouting(); + + appApi.UseAuthentication(); + appApi.UseAuthorization(); + + appApi.UseSquidexOpenApi(); + + appApi.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + }); + } + } +} diff --git a/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml b/backend/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml similarity index 100% rename from src/Squidex/Areas/Api/Views/Shared/Docs.cshtml rename to backend/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml diff --git a/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs similarity index 100% rename from src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs rename to backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs diff --git a/src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs similarity index 100% rename from src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs rename to backend/src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs new file mode 100644 index 000000000..60d8d7add --- /dev/null +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Squidex.Areas.Frontend.Middlewares +{ + public sealed class WebpackMiddleware + { + private const string WebpackUrl = "http://localhost:3000/index.html"; + private readonly RequestDelegate next; + + public WebpackMiddleware(RequestDelegate next) + { + this.next = next; + } + + public async Task Invoke(HttpContext context) + { + if (context.IsIndex() && context.Response.StatusCode != 304) + { + using (var client = new HttpClient()) + { + var result = await client.GetAsync(WebpackUrl); + + context.Response.StatusCode = (int)result.StatusCode; + + if (result.IsSuccessStatusCode) + { + var html = await result.Content.ReadAsStringAsync(); + + html = html.AdjustHtml(context); + + await context.Response.WriteAsync(html); + } + } + } + else if (context.IsHtmlPath() && context.Response.StatusCode != 304) + { + var responseBuffer = new MemoryStream(); + var responseBody = context.Response.Body; + + context.Response.Body = responseBuffer; + + await next(context); + + if (context.Response.StatusCode != 304) + { + context.Response.Body = responseBody; + + var html = Encoding.UTF8.GetString(responseBuffer.ToArray()); + + html = html.AdjustHtml(context); + + context.Response.ContentLength = Encoding.UTF8.GetByteCount(html); + context.Response.Body = responseBody; + + await context.Response.WriteAsync(html); + } + } + else + { + await next(context); + } + } + } +} diff --git a/backend/src/Squidex/Areas/Frontend/Startup.cs b/backend/src/Squidex/Areas/Frontend/Startup.cs new file mode 100644 index 000000000..9df94bfe1 --- /dev/null +++ b/backend/src/Squidex/Areas/Frontend/Startup.cs @@ -0,0 +1,88 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Net.Http.Headers; +using Squidex.Areas.Frontend.Middlewares; +using Squidex.Pipeline.Squid; + +namespace Squidex.Areas.Frontend +{ + public static class Startup + { + public static void ConfigureFrontend(this IApplicationBuilder app) + { + var environment = app.ApplicationServices.GetRequiredService(); + + app.UseMiddleware(); + + app.Use((context, next) => + { + if (context.Request.Path == "/client-callback-popup") + { + context.Request.Path = new PathString("/client-callback-popup.html"); + } + else if (context.Request.Path == "/client-callback-silent") + { + context.Request.Path = new PathString("/client-callback-silent.html"); + } + else if (!Path.HasExtension(context.Request.Path.Value)) + { + if (environment.IsDevelopment()) + { + context.Request.Path = new PathString("/index.html"); + } + else + { + context.Request.Path = new PathString("/build/index.html"); + } + } + + return next(); + }); + + if (environment.IsDevelopment()) + { + app.UseMiddleware(); + } + else + { + app.UseMiddleware(); + } + + app.UseStaticFiles(new StaticFileOptions + { + OnPrepareResponse = context => + { + var response = context.Context.Response; + var responseHeaders = response.GetTypedHeaders(); + + if (!string.Equals(response.ContentType, "text/html", StringComparison.OrdinalIgnoreCase)) + { + responseHeaders.CacheControl = new CacheControlHeaderValue + { + MaxAge = TimeSpan.FromDays(60) + }; + } + else + { + responseHeaders.CacheControl = new CacheControlHeaderValue + { + NoCache = true + }; + } + } + }); + } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.crt b/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.crt new file mode 100644 index 000000000..1f58f13a9 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCzCCAvOgAwIBAgIULb5or4cjujSTMg9dZOFJtUU0h2MwDQYJKoZIhvcNAQEL +BQAwFTETMBEGA1UEAwwKc3F1aWRleC5pbzAeFw0xOTEwMjUxODAwMzJaFw0yOTEw +MjIxODAwMzJaMBUxEzARBgNVBAMMCnNxdWlkZXguaW8wggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDkCBwdntYhthsvwj7TobnKplejrvZvkMT79SGx4tXe +5eFDpuMGqljMUzZoUaaQsy4mRe/4c9bMmMlpRmIPpJfYAYDxGzSm9N0FjUZqGJPq +kRNTNKqTPeFwl7vdn+MuveLWyOc+mSYWkPWg9WURdN5yNtDi1IUrcj+XsUqH0AQi +eEiiQJlSUF8NkdvviGv906uOG/59NwpvCsyHwV3dOR2GFHvi2RIg/M+4d8Vz2AVm +KvXVoQtxySweXRXNKvoAeKdvQ4prHc0oHo57vhW/nDV++WUGZ7LOnpr4OgIyrvM/ +0LFfSIdgJ2rK/+yKNQLJKZbq0DD942PvQapEC9Xfuuqw1S6wHXgf3CzPuoNX4sDI +2rIw84AjH0gB7IuwC0DVqcbI++Xv6H3HzpD0i1ONaHztmp5tCx9v6dkJ0ctX6vzC +SwMxdUerB9XELifOF9CqnSjUzOiAuQ9yTE0iqb6jRu5JTGeKTtmFQ6T0YUuqiPip +H3zblloGKo3mQbVumvfELb0wTs3Ay0jaczjD0aM3fRKav+6b6Qu+tDnlxL/yPde2 +SMxDwIKIH8eCAa3+8OU9VSZ0+2DS/pu1vXdsXqa5EXJJ7Ej/NyQe+nKjOG/TfeYG +u+GAO5/pJYE4lWq77hZd9ylm21dbs+X4X3uZFOsGb0BCoQADi/QFwexcz+hOmQuX +BwIDAQABo1MwUTAdBgNVHQ4EFgQUuLxsPj+ueDfckXVeG6aRFM0KVc0wHwYDVR0j +BBgwFoAUuLxsPj+ueDfckXVeG6aRFM0KVc0wDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEA0ePG4Xp29yWkDHO2Zp6dG/uVy15sEVpG25VlwEuXzdnb +VDN1++spoTLZT0HWGs9OZtCkXF2wfFL3C/az5sSn40uXy5UzoHtldEjTchSFX2tN +ulVbSiEUyxp2xEzbELIPaELhecPUyJMKUTHOLfrLaKWFC26KQK+R5E4mdx0nIZ73 +9GZFDA7okfzqkl3CeLhHfkKrPy/dLcz9doBkca4scSmgJcMQvS2sC7wVrcTtfcsh +cefQ8hMR4vfVQHl0mU7cHUJR1U7sSrXh/pOjrzX/0k/VGO+pQtDVnT8YZXRx+w0S +4nRz60nUxIDbad/xld71YV6L3rWYy2/7MIbCb71mszc6SdQtV/+lc3yJJdvNmNtc +xlpirsI1vr3yfPcuYuS8i0dqPlh7Rn+wlrqFNlu6pgpB5uhVCHXfkf3TATGJpyi3 +lN/f98Du6ZDvsIFk6loWJ/SkRAgX4un3mVEeonDMSaWAHwfPdoMXE5ViCgKLPo+B +HHM5bmZmUk25mgFoiRYx/jnw2Ym+Vsyw6SI0+kQLLoAfP/pP39rWe+MbSIhhDXC6 +5lP5IebfzEI10PAg9UrgSDShAT2E4fFaMx0mRi0dwhRgBJEW/EEjjXd8+QjXPuPF +GqU6YTf/rcDQB4cT/GaBkUar3qanmBESAabMoabZ0EDVprwrrfbqx9bDsOz4J9k= +-----END CERTIFICATE----- diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.key b/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.key new file mode 100644 index 000000000..2fd9618e8 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +4VvGnkGGbUe3PWfxA+yBJqdyprlXbaY= +-----END PRIVATE KEY----- diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.pfx b/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.pfx new file mode 100644 index 0000000000000000000000000000000000000000..b3032a3e2a9c1c3a5a761122ffa452f6e046a8da GIT binary patch literal 5389 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 identityOptions) + : base(log) + { + this.serviceProvider = serviceProvider; + + this.identityOptions = identityOptions.Value; + } + + protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) + { + IdentityModelEventSource.ShowPII = identityOptions.ShowPII; + + if (identityOptions.IsAdminConfigured()) + { + using (var scope = serviceProvider.CreateScope()) + { + var userManager = scope.ServiceProvider.GetRequiredService>(); + var userFactory = scope.ServiceProvider.GetRequiredService(); + + var adminEmail = identityOptions.AdminEmail; + var adminPass = identityOptions.AdminPassword; + + if (userManager.SupportsQueryableUsers && !userManager.Users.Any()) + { + try + { + var values = new UserValues + { + Email = adminEmail, + Password = adminPass, + Permissions = new PermissionSet(Permissions.Admin), + DisplayName = adminEmail + }; + + await userManager.CreateAsync(userFactory, values); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "createAdmin") + .WriteProperty("status", "failed")); + } + } + } + } + } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs new file mode 100644 index 000000000..aca245734 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Logging; +using Squidex.Config; +using Squidex.Domain.Users; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Security; +using Squidex.Shared; + +namespace Squidex.Areas.IdentityServer.Config +{ + public static class IdentityServerExtensions + { + public static IApplicationBuilder UseSquidexIdentityServer(this IApplicationBuilder app) + { + app.UseIdentityServer(); + + return app; + } + + public static IServiceProvider UseSquidexAdmin(this IServiceProvider services) + { + var options = services.GetRequiredService>().Value; + + IdentityModelEventSource.ShowPII = options.ShowPII; + + var userManager = services.GetRequiredService>(); + var userFactory = services.GetRequiredService(); + + var log = services.GetRequiredService(); + + if (options.IsAdminConfigured()) + { + var adminEmail = options.AdminEmail; + var adminPass = options.AdminPassword; + + Task.Run(async () => + { + if (userManager.SupportsQueryableUsers && !userManager.Users.Any()) + { + try + { + var values = new UserValues + { + Email = adminEmail, + Password = adminPass, + Permissions = new PermissionSet(Permissions.Admin), + DisplayName = adminEmail + }; + + await userManager.CreateAsync(userFactory, values); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "createAdmin") + .WriteProperty("status", "failed")); + } + } + }).Wait(); + } + + return services; + } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs new file mode 100644 index 000000000..8127fbe1a --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs @@ -0,0 +1,120 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using IdentityModel; +using IdentityServer4.Models; +using IdentityServer4.Stores; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.DataProtection.KeyManagement; +using Microsoft.AspNetCore.DataProtection.Repositories; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Domain.Users; +using Squidex.Shared.Identity; +using Squidex.Web; +using Squidex.Web.Pipeline; + +namespace Squidex.Areas.IdentityServer.Config +{ + public static class IdentityServerServices + { + public static void AddSquidexIdentityServer(this IServiceCollection services) + { + X509Certificate2 certificate; + + var assembly = typeof(IdentityServerServices).Assembly; + + using (var certificateStream = assembly.GetManifestResourceStream("Squidex.Areas.IdentityServer.Config.Cert.IdentityCert.pfx")) + { + var certData = new byte[certificateStream!.Length]; + + certificateStream.Read(certData, 0, certData.Length); + certificate = new X509Certificate2(certData, "password", + X509KeyStorageFlags.MachineKeySet | + X509KeyStorageFlags.PersistKeySet | + X509KeyStorageFlags.Exportable); + } + + services.AddSingleton>(s => + { + return new ConfigureOptions(options => + { + options.XmlRepository = s.GetRequiredService(); + }); + }); + + services.AddDataProtection().SetApplicationName("Squidex"); + + services.AddSingleton(GetApiResources()); + services.AddSingleton(GetIdentityResources()); + + services.AddIdentity() + .AddDefaultTokenProviders(); + services.AddSingleton, + PwnedPasswordValidator>(); + services.AddScoped, + UserClaimsPrincipalFactoryWithEmail>(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddIdentityServer(options => + { + options.UserInteraction.ErrorUrl = "/error/"; + }) + .AddAspNetIdentity() + .AddInMemoryApiResources(GetApiResources()) + .AddInMemoryIdentityResources(GetIdentityResources()) + .AddSigningCredential(certificate); + } + + private static IEnumerable GetApiResources() + { + yield return new ApiResource(Constants.ApiScope) + { + UserClaims = new List + { + JwtClaimTypes.Email, + JwtClaimTypes.Role, + SquidexClaimTypes.Permissions + } + }; + } + + private static IEnumerable GetIdentityResources() + { + yield return new IdentityResources.OpenId(); + yield return new IdentityResources.Profile(); + yield return new IdentityResources.Email(); + yield return new IdentityResource(Constants.RoleScope, + new[] + { + JwtClaimTypes.Role + }); + yield return new IdentityResource(Constants.PermissionsScope, + new[] + { + SquidexClaimTypes.Permissions + }); + yield return new IdentityResource(Constants.ProfileScope, + new[] + { + SquidexClaimTypes.DisplayName, + SquidexClaimTypes.PictureUrl + }); + } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs b/backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs new file mode 100644 index 000000000..86642263c --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs @@ -0,0 +1,239 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using IdentityServer4; +using IdentityServer4.Models; +using IdentityServer4.Stores; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Config; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Users; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; +using Squidex.Web; + +namespace Squidex.Areas.IdentityServer.Config +{ + public class LazyClientStore : IClientStore + { + private readonly IServiceProvider serviceProvider; + private readonly IAppProvider appProvider; + private readonly Dictionary staticClients = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public LazyClientStore( + IServiceProvider serviceProvider, + IOptions urlsOptions, + IOptions identityOptions, + IAppProvider appProvider) + { + Guard.NotNull(appProvider); + Guard.NotNull(identityOptions); + Guard.NotNull(serviceProvider); + Guard.NotNull(urlsOptions); + + this.serviceProvider = serviceProvider; + + this.appProvider = appProvider; + + CreateStaticClients(urlsOptions, identityOptions); + } + + public async Task FindClientByIdAsync(string clientId) + { + var client = staticClients.GetOrDefault(clientId); + + if (client != null) + { + return client; + } + + var (appName, appClientId) = clientId.GetClientParts(); + + if (!string.IsNullOrWhiteSpace(appName) && !string.IsNullOrWhiteSpace(appClientId)) + { + var app = await appProvider.GetAppAsync(appName); + + var appClient = app?.Clients.GetOrDefault(appClientId); + + if (appClient != null) + { + return CreateClientFromApp(clientId, appClient); + } + } + + using (var scope = serviceProvider.CreateScope()) + { + var userManager = scope.ServiceProvider.GetRequiredService>(); + + var user = await userManager.FindByIdWithClaimsAsync(clientId); + + if (!string.IsNullOrWhiteSpace(user?.ClientSecret())) + { + return CreateClientFromUser(user); + } + } + + return null; + } + + private static Client CreateClientFromUser(UserWithClaims user) + { + return new Client + { + ClientId = user.Id, + ClientName = $"{user.Email} Client", + ClientClaimsPrefix = null, + ClientSecrets = new List + { + new Secret(user.ClientSecret().Sha256()) + }, + AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, + AllowedGrantTypes = GrantTypes.ClientCredentials, + AllowedScopes = new List + { + Constants.ApiScope, + Constants.RoleScope, + Constants.PermissionsScope + }, + Claims = new List + { + new Claim(OpenIdClaims.Subject, user.Id) + } + }; + } + + private static Client CreateClientFromApp(string id, AppClient appClient) + { + return new Client + { + ClientId = id, + ClientName = id, + ClientSecrets = new List + { + new Secret(appClient.Secret.Sha256()) + }, + AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, + AllowedGrantTypes = GrantTypes.ClientCredentials, + AllowedScopes = new List + { + Constants.ApiScope, + Constants.RoleScope, + Constants.PermissionsScope + } + }; + } + + private void CreateStaticClients(IOptions urlsOptions, IOptions identityOptions) + { + foreach (var client in CreateStaticClients(urlsOptions.Value, identityOptions.Value)) + { + staticClients[client.ClientId] = client; + } + } + + private static IEnumerable CreateStaticClients(UrlsOptions urlsOptions, MyIdentityOptions identityOptions) + { + var frontendId = Constants.FrontendClient; + + yield return new Client + { + ClientId = frontendId, + ClientName = frontendId, + RedirectUris = new List + { + urlsOptions.BuildUrl("login;"), + urlsOptions.BuildUrl("client-callback-silent", false), + urlsOptions.BuildUrl("client-callback-popup", false) + }, + PostLogoutRedirectUris = new List + { + urlsOptions.BuildUrl("logout", false) + }, + AllowAccessTokensViaBrowser = true, + AllowedGrantTypes = GrantTypes.Implicit, + AllowedScopes = new List + { + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile, + IdentityServerConstants.StandardScopes.Email, + Constants.ApiScope, + Constants.PermissionsScope, + Constants.ProfileScope, + Constants.RoleScope + }, + RequireConsent = false + }; + + var internalClient = Constants.InternalClientId; + + yield return new Client + { + ClientId = internalClient, + ClientName = internalClient, + ClientSecrets = new List + { + new Secret(Constants.InternalClientSecret) + }, + RedirectUris = new List + { + urlsOptions.BuildUrl($"{Constants.PortalPrefix}/signin-internal", false), + urlsOptions.BuildUrl($"{Constants.OrleansPrefix}/signin-internal", false) + }, + AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, + AllowedGrantTypes = GrantTypes.ImplicitAndClientCredentials, + AllowedScopes = new List + { + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile, + IdentityServerConstants.StandardScopes.Email, + Constants.ApiScope, + Constants.PermissionsScope, + Constants.ProfileScope, + Constants.RoleScope + }, + RequireConsent = false + }; + + if (identityOptions.IsAdminClientConfigured()) + { + var id = identityOptions.AdminClientId; + + yield return new Client + { + ClientId = id, + ClientName = id, + ClientSecrets = new List + { + new Secret(identityOptions.AdminClientSecret.Sha256()) + }, + AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, + AllowedGrantTypes = GrantTypes.ClientCredentials, + AllowedScopes = new List + { + Constants.ApiScope, + Constants.RoleScope, + Constants.PermissionsScope + }, + Claims = new List + { + new Claim(SquidexClaimTypes.Permissions, Permissions.All) + } + }; + } + } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs new file mode 100644 index 000000000..983e56954 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs @@ -0,0 +1,438 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Security; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using IdentityServer4.Models; +using IdentityServer4.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Squidex.Config; +using Squidex.Domain.Users; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; +using Squidex.Shared; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; +using Squidex.Web; + +namespace Squidex.Areas.IdentityServer.Controllers.Account +{ + public sealed class AccountController : IdentityServerController + { + private readonly SignInManager signInManager; + private readonly UserManager userManager; + private readonly IUserFactory userFactory; + private readonly IUserEvents userEvents; + private readonly MyIdentityOptions identityOptions; + private readonly ISemanticLog log; + private readonly IIdentityServerInteractionService interactions; + + public AccountController( + SignInManager signInManager, + UserManager userManager, + IUserFactory userFactory, + IUserEvents userEvents, + IOptions identityOptions, + ISemanticLog log, + IIdentityServerInteractionService interactions) + { + this.log = log; + this.userEvents = userEvents; + this.userManager = userManager; + this.userFactory = userFactory; + this.interactions = interactions; + this.identityOptions = identityOptions.Value; + this.signInManager = signInManager; + } + + [HttpGet] + [Route("account/error/")] + public IActionResult LoginError() + { + throw new InvalidOperationException(); + } + + [HttpGet] + [Route("account/forbidden/")] + public IActionResult Forbidden() + { + throw new SecurityException("User is not allowed to login."); + } + + [HttpGet] + [Route("account/lockedout/")] + public IActionResult LockedOut() + { + return View(); + } + + [HttpGet] + [Route("account/accessdenied/")] + public IActionResult AccessDenied() + { + return View(); + } + + [HttpGet] + [Route("account/logout-completed/")] + public IActionResult LogoutCompleted() + { + return View(); + } + + [HttpGet] + [Route("account/consent/")] + public IActionResult Consent(string? returnUrl = null) + { + return View(new ConsentVM { PrivacyUrl = identityOptions.PrivacyUrl, ReturnUrl = returnUrl }); + } + + [HttpPost] + [Route("account/consent/")] + public async Task Consent(ConsentModel model, string? returnUrl = null) + { + if (!model.ConsentToCookies) + { + ModelState.AddModelError(nameof(model.ConsentToCookies), "You have to give consent."); + } + + if (!model.ConsentToPersonalInformation) + { + ModelState.AddModelError(nameof(model.ConsentToPersonalInformation), "You have to give consent."); + } + + if (!ModelState.IsValid) + { + var vm = new ConsentVM { PrivacyUrl = identityOptions.PrivacyUrl, ReturnUrl = returnUrl }; + + return View(vm); + } + + var user = await userManager.GetUserWithClaimsAsync(User); + + if (user == null) + { + throw new DomainException("Cannot find user."); + } + + var update = new UserValues + { + Consent = true, + ConsentForEmails = model.ConsentToAutomatedEmails + }; + + await userManager.UpdateAsync(user.Id, update); + + userEvents.OnConsentGiven(user); + + return RedirectToReturnUrl(returnUrl); + } + + [HttpGet] + [Route("account/logout/")] + public async Task Logout(string logoutId) + { + var context = await interactions.GetLogoutContextAsync(logoutId); + + await signInManager.SignOutAsync(); + + return RedirectToLogoutUrl(context); + } + + [HttpGet] + [Route("account/logout-redirect/")] + public async Task LogoutRedirect() + { + await signInManager.SignOutAsync(); + + return RedirectToAction(nameof(LogoutCompleted)); + } + + [HttpGet] + [Route("account/signup/")] + public Task Signup(string? returnUrl = null) + { + return LoginViewAsync(returnUrl, false, false); + } + + [HttpGet] + [Route("account/login/")] + [ClearCookies] + public Task Login(string? returnUrl = null) + { + return LoginViewAsync(returnUrl, true, false); + } + + [HttpPost] + [Route("account/login/")] + public async Task Login(LoginModel model, string? returnUrl = null) + { + if (!ModelState.IsValid) + { + return await LoginViewAsync(returnUrl, true, true); + } + + var result = await signInManager.PasswordSignInAsync(model.Email, model.Password, true, true); + + if (!result.Succeeded) + { + return await LoginViewAsync(returnUrl, true, true); + } + else + { + return RedirectToReturnUrl(returnUrl); + } + } + + private async Task LoginViewAsync(string? returnUrl, bool isLogin, bool isFailed) + { + var allowPasswordAuth = identityOptions.AllowPasswordAuth; + + var externalProviders = await signInManager.GetExternalProvidersAsync(); + + if (externalProviders.Count == 1 && !allowPasswordAuth) + { + var provider = externalProviders[0].AuthenticationScheme; + + var properties = + signInManager.ConfigureExternalAuthenticationProperties(provider, + Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl })); + + return Challenge(properties, provider); + } + + var vm = new LoginVM + { + ExternalProviders = externalProviders, + IsLogin = isLogin, + IsFailed = isFailed, + HasPasswordAuth = allowPasswordAuth, + HasPasswordAndExternal = allowPasswordAuth && externalProviders.Any(), + ReturnUrl = returnUrl + }; + + return View(nameof(Login), vm); + } + + [HttpPost] + [Route("account/external/")] + public IActionResult External(string provider, string? returnUrl = null) + { + var properties = + signInManager.ConfigureExternalAuthenticationProperties(provider, + Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl })); + + return Challenge(properties, provider); + } + + [HttpGet] + [Route("account/external-callback/")] + public async Task ExternalCallback(string? returnUrl = null) + { + var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(); + + if (externalLogin == null) + { + return RedirectToAction(nameof(Login)); + } + + var result = await signInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); + + if (!result.Succeeded && result.IsLockedOut) + { + return View(nameof(LockedOut)); + } + + var isLoggedIn = result.Succeeded; + + UserWithClaims? user; + + if (isLoggedIn) + { + user = await userManager.FindByLoginWithClaimsAsync(externalLogin.LoginProvider, externalLogin.ProviderKey); + } + else + { + var email = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value; + + user = await userManager.FindByEmailWithClaimsAsyncAsync(email); + + if (user != null) + { + isLoggedIn = + await AddLoginAsync(user, externalLogin) && + await AddClaimsAsync(user, externalLogin, email) && + await LoginAsync(externalLogin); + } + else + { + user = new UserWithClaims(userFactory.Create(email), new List()); + + var isFirst = userManager.Users.LongCount() == 0; + + isLoggedIn = + await AddUserAsync(user) && + await AddLoginAsync(user, externalLogin) && + await AddClaimsAsync(user, externalLogin, email, isFirst) && + await LockAsync(user, isFirst) && + await LoginAsync(externalLogin); + + userEvents.OnUserRegistered(user); + + if (await userManager.IsLockedOutAsync(user.Identity)) + { + return View(nameof(LockedOut)); + } + } + } + + if (!isLoggedIn) + { + return RedirectToAction(nameof(Login)); + } + else if (user != null && !user.HasConsent() && !identityOptions.NoConsent) + { + return RedirectToAction(nameof(Consent), new { returnUrl }); + } + else + { + return RedirectToReturnUrl(returnUrl); + } + } + + private Task AddLoginAsync(UserWithClaims user, UserLoginInfo externalLogin) + { + return MakeIdentityOperation(() => userManager.AddLoginAsync(user.Identity, externalLogin)); + } + + private Task AddUserAsync(UserWithClaims user) + { + return MakeIdentityOperation(() => userManager.CreateAsync(user.Identity)); + } + + private async Task LoginAsync(UserLoginInfo externalLogin) + { + var result = await signInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); + + return result.Succeeded; + } + + private Task LockAsync(UserWithClaims user, bool isFirst) + { + if (isFirst || !identityOptions.LockAutomatically) + { + return TaskHelper.True; + } + + return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user.Identity, DateTimeOffset.UtcNow.AddYears(100))); + } + + private Task AddClaimsAsync(UserWithClaims user, ExternalLoginInfo externalLogin, string email, bool isFirst = false) + { + var newClaims = new List(); + + void AddClaim(Claim claim) + { + newClaims.Add(claim); + + user.Claims.Add(claim); + } + + foreach (var squidexClaim in externalLogin.Principal.GetSquidexClaims()) + { + AddClaim(squidexClaim); + } + + if (!user.HasPictureUrl()) + { + AddClaim(new Claim(SquidexClaimTypes.PictureUrl, GravatarHelper.CreatePictureUrl(email))); + } + + if (!user.HasDisplayName()) + { + AddClaim(new Claim(SquidexClaimTypes.DisplayName, email)); + } + + if (isFirst) + { + AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.Admin)); + } + + return MakeIdentityOperation(() => userManager.SyncClaimsAsync(user.Identity, newClaims)); + } + + private IActionResult RedirectToLogoutUrl(LogoutRequest context) + { + if (!string.IsNullOrWhiteSpace(context.PostLogoutRedirectUri)) + { + return Redirect(context.PostLogoutRedirectUri); + } + else + { + return Redirect("~/../"); + } + } + + private IActionResult RedirectToReturnUrl(string? returnUrl) + { + if (!string.IsNullOrWhiteSpace(returnUrl)) + { + return Redirect(returnUrl); + } + else + { + return Redirect("~/../"); + } + } + + private async Task MakeIdentityOperation(Func> action, [CallerMemberName] string? operationName = null) + { + try + { + var result = await action(); + + if (!result.Succeeded) + { + var errorMessageBuilder = new StringBuilder(); + + foreach (var error in result.Errors) + { + errorMessageBuilder.Append(error.Code); + errorMessageBuilder.Append(": "); + errorMessageBuilder.AppendLine(error.Description); + } + + var errorMessage = errorMessageBuilder.ToString(); + + log.LogError((operationName, errorMessage), (ctx, w) => w + .WriteProperty("action", ctx.operationName) + .WriteProperty("status", "Failed") + .WriteProperty("message", ctx.errorMessage)); + } + + return result.Succeeded; + } + catch (Exception ex) + { + log.LogError(ex, operationName, (logOperationName, w) => w + .WriteProperty("action", logOperationName) + .WriteProperty("status", "Failed")); + + return false; + } + } + } +} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs new file mode 100644 index 000000000..92aba14ae --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Areas.IdentityServer.Controllers.Account +{ + public sealed class ConsentVM + { + public string? ReturnUrl { get; set; } + + public string? PrivacyUrl { get; set; } + } +} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs new file mode 100644 index 000000000..35d8cc8f5 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Areas.IdentityServer.Controllers.Account +{ + public class LoginVM + { + public string? ReturnUrl { get; set; } + + public bool IsLogin { get; set; } + + public bool IsFailed { get; set; } + + public bool HasPasswordAuth { get; set; } + + public bool HasPasswordAndExternal { get; set; } + + public IReadOnlyList ExternalProviders { get; set; } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs new file mode 100644 index 000000000..528d504d2 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using IdentityServer4.Models; +using IdentityServer4.Services; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Squidex.Infrastructure; + +namespace Squidex.Areas.IdentityServer.Controllers.Error +{ + public sealed class ErrorController : IdentityServerController + { + private readonly IIdentityServerInteractionService interaction; + private readonly SignInManager signInManager; + + public ErrorController(IIdentityServerInteractionService interaction, SignInManager signInManager) + { + this.interaction = interaction; + this.signInManager = signInManager; + } + + [Route("error/")] + public async Task Error(string? errorId = null) + { + await signInManager.SignOutAsync(); + + var vm = new ErrorViewModel(); + + if (!string.IsNullOrWhiteSpace(errorId)) + { + var message = await interaction.GetErrorContextAsync(errorId); + + if (message != null) + { + vm.Error = message; + } + } + + if (vm.Error == null) + { + var error = HttpContext.Features.Get()?.Error; + + if (error is DomainException exception) + { + vm.Error = new ErrorMessage { ErrorDescription = exception.Message }; + } + else if (error?.InnerException is DomainException exception2) + { + vm.Error = new ErrorMessage { ErrorDescription = exception2.Message }; + } + } + + return View("Error", vm); + } + } +} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorViewModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorViewModel.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorViewModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorViewModel.cs diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs new file mode 100644 index 000000000..c7daea59b --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Identity; + +namespace Squidex.Areas.IdentityServer.Controllers +{ + public static class Extensions + { + public static async Task GetExternalLoginInfoWithDisplayNameAsync(this SignInManager signInManager, string? expectedXsrf = null) + { + var externalLogin = await signInManager.GetExternalLoginInfoAsync(expectedXsrf); + + var email = externalLogin.Principal.FindFirst(ClaimTypes.Email)?.Value; + + if (string.IsNullOrWhiteSpace(email)) + { + throw new InvalidOperationException("External provider does not provide email claim."); + } + + externalLogin.ProviderDisplayName = email; + + return externalLogin; + } + + public static async Task> GetExternalProvidersAsync(this SignInManager signInManager) + { + var externalSchemes = await signInManager.GetExternalAuthenticationSchemesAsync(); + + var externalProviders = + externalSchemes.Where(x => x.Name != OpenIdConnectDefaults.AuthenticationScheme) + .Select(x => new ExternalProvider(x.Name, x.DisplayName)).ToList(); + + return externalProviders; + } + } +} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs diff --git a/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs new file mode 100644 index 000000000..41e22fa3a --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs @@ -0,0 +1,235 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Squidex.Config; +using Squidex.Domain.Users; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; + +namespace Squidex.Areas.IdentityServer.Controllers.Profile +{ + [Authorize] + public sealed class ProfileController : IdentityServerController + { + private readonly SignInManager signInManager; + private readonly UserManager userManager; + private readonly IUserPictureStore userPictureStore; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly MyIdentityOptions identityOptions; + + public ProfileController( + SignInManager signInManager, + UserManager userManager, + IUserPictureStore userPictureStore, + IAssetThumbnailGenerator assetThumbnailGenerator, + IOptions identityOptions) + { + this.signInManager = signInManager; + this.identityOptions = identityOptions.Value; + this.userManager = userManager; + this.userPictureStore = userPictureStore; + this.assetThumbnailGenerator = assetThumbnailGenerator; + } + + [HttpGet] + [Route("/account/profile/")] + public async Task Profile(string? successMessage = null) + { + var user = await userManager.GetUserWithClaimsAsync(User); + + return View(await GetProfileVM(user, successMessage: successMessage)); + } + + [HttpPost] + [Route("/account/profile/login-add/")] + public async Task AddLogin(string provider) + { + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + var properties = + signInManager.ConfigureExternalAuthenticationProperties(provider, + Url.Action(nameof(AddLoginCallback)), userManager.GetUserId(User)); + + return Challenge(properties, provider); + } + + [HttpGet] + [Route("/account/profile/login-add-callback/")] + public Task AddLoginCallback() + { + return MakeChangeAsync(user => AddLoginAsync(user), + "Login added successfully."); + } + + [HttpPost] + [Route("/account/profile/update/")] + public Task UpdateProfile(ChangeProfileModel model) + { + return MakeChangeAsync(user => userManager.UpdateSafeAsync(user.Identity, model.ToValues()), + "Account updated successfully."); + } + + [HttpPost] + [Route("/account/profile/login-remove/")] + public Task RemoveLogin(RemoveLoginModel model) + { + return MakeChangeAsync(user => userManager.RemoveLoginAsync(user.Identity, model.LoginProvider, model.ProviderKey), + "Login provider removed successfully."); + } + + [HttpPost] + [Route("/account/profile/password-set/")] + public Task SetPassword(SetPasswordModel model) + { + return MakeChangeAsync(user => userManager.AddPasswordAsync(user.Identity, model.Password), + "Password set successfully."); + } + + [HttpPost] + [Route("/account/profile/password-change/")] + public Task ChangePassword(ChangePasswordModel model) + { + return MakeChangeAsync(user => userManager.ChangePasswordAsync(user.Identity, model.OldPassword, model.Password), + "Password changed successfully."); + } + + [HttpPost] + [Route("/account/profile/generate-client-secret/")] + public Task GenerateClientSecret() + { + return MakeChangeAsync(user => userManager.GenerateClientSecretAsync(user.Identity), + "Client secret generated successfully."); + } + + [HttpPost] + [Route("/account/profile/upload-picture/")] + public Task UploadPicture(List file) + { + return MakeChangeAsync(user => UpdatePictureAsync(file, user), + "Picture uploaded successfully."); + } + + private async Task AddLoginAsync(UserWithClaims user) + { + var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(userManager.GetUserId(User)); + + return await userManager.AddLoginAsync(user.Identity, externalLogin); + } + + private async Task UpdatePictureAsync(List file, UserWithClaims user) + { + if (file.Count != 1) + { + return IdentityResult.Failed(new IdentityError { Description = "Please upload a single file." }); + } + + using (var thumbnailStream = new MemoryStream()) + { + try + { + await assetThumbnailGenerator.CreateThumbnailAsync(file[0].OpenReadStream(), thumbnailStream, 128, 128, "Crop"); + + thumbnailStream.Position = 0; + } + catch + { + return IdentityResult.Failed(new IdentityError { Description = "Picture is not a valid image." }); + } + + await userPictureStore.UploadAsync(user.Id, thumbnailStream); + } + + return await userManager.UpdateSafeAsync(user.Identity, new UserValues { PictureUrl = SquidexClaimTypes.PictureUrlStore }); + } + + private async Task MakeChangeAsync(Func> action, string successMessage, ChangeProfileModel? model = null) + { + var user = await userManager.GetUserWithClaimsAsync(User); + + if (user == null) + { + throw new DomainException("Cannot find user."); + } + + if (!ModelState.IsValid) + { + return View(nameof(Profile), await GetProfileVM(user, model)); + } + + string errorMessage; + try + { + var result = await action(user); + + if (result.Succeeded) + { + await signInManager.SignInAsync(user.Identity, true); + + return RedirectToAction(nameof(Profile), new { successMessage }); + } + + errorMessage = string.Join(". ", result.Errors.Select(x => x.Description)); + } + catch + { + errorMessage = "An unexpected exception occurred."; + } + + return View(nameof(Profile), await GetProfileVM(user, model, errorMessage)); + } + + private async Task GetProfileVM(UserWithClaims? user, ChangeProfileModel? model = null, string? errorMessage = null, string? successMessage = null) + { + if (user == null) + { + throw new DomainException("Cannot find user."); + } + + var taskForProviders = signInManager.GetExternalProvidersAsync(); + var taskForPassword = userManager.HasPasswordAsync(user.Identity); + var taskForLogins = userManager.GetLoginsAsync(user.Identity); + + await Task.WhenAll(taskForProviders, taskForPassword, taskForLogins); + + var result = new ProfileVM + { + Id = user.Id, + ClientSecret = user.ClientSecret()!, + Email = user.Email, + ErrorMessage = errorMessage, + ExternalLogins = taskForLogins.Result, + ExternalProviders = taskForProviders.Result, + DisplayName = user.DisplayName()!, + IsHidden = user.IsHidden(), + HasPassword = taskForPassword.Result, + HasPasswordAuth = identityOptions.AllowPasswordAuth, + SuccessMessage = successMessage + }; + + if (model != null) + { + SimpleMapper.Map(model, result); + } + + return result; + } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs new file mode 100644 index 000000000..d2f377e8d --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Microsoft.AspNetCore.Identity; + +namespace Squidex.Areas.IdentityServer.Controllers.Profile +{ + public sealed class ProfileVM + { + public string Id { get; set; } + + public string Email { get; set; } + + public string DisplayName { get; set; } + + public string? ClientSecret { get; set; } + + public string? ErrorMessage { get; set; } + + public string? SuccessMessage { get; set; } + + public bool IsHidden { get; set; } + + public bool HasPassword { get; set; } + + public bool HasPasswordAuth { get; set; } + + public IList ExternalLogins { get; set; } + + public IList ExternalProviders { get; set; } + } +} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs diff --git a/backend/src/Squidex/Areas/IdentityServer/Startup.cs b/backend/src/Squidex/Areas/IdentityServer/Startup.cs new file mode 100644 index 000000000..75a89085e --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Startup.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Squidex.Areas.IdentityServer.Config; +using Squidex.Web; + +namespace Squidex.Areas.IdentityServer +{ + public static class Startup + { + public static void ConfigureIdentityServer(this IApplicationBuilder app) + { + var environment = app.ApplicationServices.GetRequiredService(); + + app.Map(Constants.IdentityServerPrefix, identityApp => + { + if (!environment.IsDevelopment()) + { + identityApp.UseDeveloperExceptionPage(); + } + else + { + identityApp.UseExceptionHandler("/error"); + } + + identityApp.UseRouting(); + + identityApp.UseAuthentication(); + identityApp.UseAuthorization(); + + identityApp.UseSquidexIdentityServer(); + + identityApp.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + }); + } + } +} diff --git a/src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Extensions.cs b/backend/src/Squidex/Areas/IdentityServer/Views/Extensions.cs new file mode 100644 index 000000000..eb16259f5 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Extensions.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Squidex.Areas.IdentityServer.Views +{ + public static class Extensions + { + public static string? RootContentUrl(this IUrlHelper urlHelper, string contentPath) + { + if (string.IsNullOrEmpty(contentPath)) + { + return null; + } + + if (contentPath[0] == '~') + { + var segment = new PathString(contentPath.Substring(1)); + + var applicationPath = urlHelper.ActionContext.HttpContext.Request.PathBase; + + if (applicationPath.HasValue) + { + var indexOfLastPart = applicationPath.Value.LastIndexOf('/'); + + if (indexOfLastPart >= 0) + { + applicationPath = applicationPath.Value.Substring(0, indexOfLastPart); + } + } + + return applicationPath.Add(segment).Value; + } + + return contentPath; + } + } +} diff --git a/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/_ViewImports.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/_ViewImports.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/_ViewImports.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/_ViewImports.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/_ViewStart.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/_ViewStart.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/_ViewStart.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/_ViewStart.cshtml diff --git a/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs b/backend/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs similarity index 100% rename from src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs rename to backend/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs diff --git a/backend/src/Squidex/Areas/OrleansDashboard/Startup.cs b/backend/src/Squidex/Areas/OrleansDashboard/Startup.cs new file mode 100644 index 000000000..9285b5ae8 --- /dev/null +++ b/backend/src/Squidex/Areas/OrleansDashboard/Startup.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Orleans; +using Squidex.Areas.OrleansDashboard.Middlewares; +using Squidex.Web; + +namespace Squidex.Areas.OrleansDashboard +{ + public static class Startup + { + public static void ConfigureOrleansDashboard(this IApplicationBuilder app) + { + app.Map(Constants.OrleansPrefix, orleansApp => + { + orleansApp.UseAuthentication(); + orleansApp.UseAuthorization(); + + orleansApp.UseMiddleware(); + orleansApp.UseOrleansDashboard(); + }); + } + } +} diff --git a/src/Squidex/Areas/Portal/Middlewares/PortalDashboardAuthenticationMiddleware.cs b/backend/src/Squidex/Areas/Portal/Middlewares/PortalDashboardAuthenticationMiddleware.cs similarity index 100% rename from src/Squidex/Areas/Portal/Middlewares/PortalDashboardAuthenticationMiddleware.cs rename to backend/src/Squidex/Areas/Portal/Middlewares/PortalDashboardAuthenticationMiddleware.cs diff --git a/backend/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs b/backend/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs new file mode 100644 index 000000000..7e6ac990b --- /dev/null +++ b/backend/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Squidex.Domain.Apps.Entities.Apps.Services; + +namespace Squidex.Areas.Portal.Middlewares +{ + public sealed class PortalRedirectMiddleware + { + private readonly IAppPlanBillingManager appPlansBillingManager; + + public PortalRedirectMiddleware(RequestDelegate next, IAppPlanBillingManager appPlansBillingManager) + { + this.appPlansBillingManager = appPlansBillingManager; + } + + public async Task Invoke(HttpContext context) + { + if (context.Request.Path == "/") + { + var userIdClaim = context.User.FindFirst(ClaimTypes.NameIdentifier); + + if (userIdClaim != null) + { + var portalLink = await appPlansBillingManager.GetPortalLinkAsync(userIdClaim.Value); + + context.Response.Redirect(portalLink); + } + } + } + } +} diff --git a/backend/src/Squidex/Areas/Portal/Startup.cs b/backend/src/Squidex/Areas/Portal/Startup.cs new file mode 100644 index 000000000..7c3f20512 --- /dev/null +++ b/backend/src/Squidex/Areas/Portal/Startup.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Squidex.Areas.Portal.Middlewares; +using Squidex.Web; + +namespace Squidex.Areas.Portal +{ + public static class Startup + { + public static void ConfigurePortal(this IApplicationBuilder app) + { + app.Map(Constants.PortalPrefix, portalApp => + { + portalApp.UseAuthentication(); + portalApp.UseAuthorization(); + + portalApp.UseMiddleware(); + portalApp.UseMiddleware(); + }); + } + } +} diff --git a/backend/src/Squidex/Config/Authentication/AuthenticationServices.cs b/backend/src/Squidex/Config/Authentication/AuthenticationServices.cs new file mode 100644 index 000000000..0e27dfb7e --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/AuthenticationServices.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.DataProtection.Repositories; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Users; + +namespace Squidex.Config.Authentication +{ + public static class AuthenticationServices + { + public static void AddSquidexAuthentication(this IServiceCollection services, IConfiguration config) + { + var identityOptions = config.GetSection("identity").Get(); + + services.AddSingletonAs() + .As(); + + services.AddAuthentication() + .AddSquidexCookies() + .AddSquidexExternalGithubAuthentication(identityOptions) + .AddSquidexExternalGoogleAuthentication(identityOptions) + .AddSquidexExternalMicrosoftAuthentication(identityOptions) + .AddSquidexExternalOdic(identityOptions) + .AddSquidexIdentityServerAuthentication(identityOptions, config); + } + + public static AuthenticationBuilder AddSquidexCookies(this AuthenticationBuilder builder) + { + builder.Services.ConfigureApplicationCookie(options => + { + options.Cookie.Name = ".sq.auth"; + }); + + return builder.AddCookie(); + } + } +} diff --git a/backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs b/backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs new file mode 100644 index 000000000..49beaa3bd --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; + +namespace Squidex.Config.Authentication +{ + public static class GithubAuthenticationServices + { + public static AuthenticationBuilder AddSquidexExternalGithubAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) + { + if (identityOptions.IsGithubAuthConfigured()) + { + authBuilder.AddGitHub(options => + { + options.ClientId = identityOptions.GithubClient; + options.ClientSecret = identityOptions.GithubSecret; + options.Events = new GithubHandler(); + }); + } + + return authBuilder; + } + } +} diff --git a/src/Squidex/Config/Authentication/GithubHandler.cs b/backend/src/Squidex/Config/Authentication/GithubHandler.cs similarity index 100% rename from src/Squidex/Config/Authentication/GithubHandler.cs rename to backend/src/Squidex/Config/Authentication/GithubHandler.cs diff --git a/backend/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs b/backend/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs new file mode 100644 index 000000000..d175d92eb --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; + +namespace Squidex.Config.Authentication +{ + public static class GoogleAuthenticationServices + { + public static AuthenticationBuilder AddSquidexExternalGoogleAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) + { + if (identityOptions.IsGoogleAuthConfigured()) + { + authBuilder.AddGoogle(options => + { + options.ClientId = identityOptions.GoogleClient; + options.ClientSecret = identityOptions.GoogleSecret; + options.Events = new GoogleHandler(); + }); + } + + return authBuilder; + } + } +} diff --git a/backend/src/Squidex/Config/Authentication/GoogleHandler.cs b/backend/src/Squidex/Config/Authentication/GoogleHandler.cs new file mode 100644 index 000000000..a8da64c84 --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/GoogleHandler.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Security.Claims; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; +using Squidex.Infrastructure.Tasks; +using Squidex.Shared.Identity; + +namespace Squidex.Config.Authentication +{ + public sealed class GoogleHandler : OAuthEvents + { + public override Task RedirectToAuthorizationEndpoint(RedirectContext context) + { + context.Response.Redirect(context.RedirectUri + "&prompt=select_account"); + + return TaskHelper.Done; + } + + public override Task CreatingTicket(OAuthCreatingTicketContext context) + { + var nameClaim = context.Identity.FindFirst(ClaimTypes.Name)?.Value; + + if (!string.IsNullOrWhiteSpace(nameClaim)) + { + context.Identity.SetDisplayName(nameClaim); + } + + string? pictureUrl = null; + + if (context.User.TryGetProperty("picture", out var picture) && picture.ValueKind == JsonValueKind.String) + { + pictureUrl = picture.GetString(); + } + + if (string.IsNullOrWhiteSpace(pictureUrl)) + { + if (context.User.TryGetProperty("image", out var image) && image.TryGetProperty("url", out var url) && url.ValueKind == JsonValueKind.String) + { + pictureUrl = url.GetString(); + } + + if (pictureUrl != null && pictureUrl.EndsWith("?sz=50", System.StringComparison.Ordinal)) + { + pictureUrl = pictureUrl[0..^6]; + } + } + + if (!string.IsNullOrWhiteSpace(pictureUrl)) + { + context.Identity.SetPictureUrl(pictureUrl); + } + + return base.CreatingTicket(context); + } + } +} diff --git a/backend/src/Squidex/Config/Authentication/IdentityServerServices.cs b/backend/src/Squidex/Config/Authentication/IdentityServerServices.cs new file mode 100644 index 000000000..bfa5720f4 --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/IdentityServerServices.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure; +using Squidex.Web; + +namespace Squidex.Config.Authentication +{ + public static class IdentityServerServices + { + public static AuthenticationBuilder AddSquidexIdentityServerAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions, IConfiguration config) + { + var apiScope = Constants.ApiScope; + + var urlsOptions = config.GetSection("urls").Get(); + + if (!string.IsNullOrWhiteSpace(urlsOptions.BaseUrl)) + { + string apiAuthorityUrl; + + if (!string.IsNullOrWhiteSpace(identityOptions.AuthorityUrl)) + { + apiAuthorityUrl = identityOptions.AuthorityUrl.BuildFullUrl(Constants.IdentityServerPrefix); + } + else + { + apiAuthorityUrl = urlsOptions.BuildUrl(Constants.IdentityServerPrefix); + } + + authBuilder.AddIdentityServerAuthentication(options => + { + options.Authority = apiAuthorityUrl; + options.ApiName = apiScope; + options.ApiSecret = null; + options.RequireHttpsMetadata = identityOptions.RequiresHttps; + }); + + authBuilder.AddOpenIdConnect(options => + { + options.Authority = apiAuthorityUrl; + options.ClientId = Constants.InternalClientId; + options.ClientSecret = Constants.InternalClientSecret; + options.CallbackPath = "/signin-internal"; + options.RequireHttpsMetadata = identityOptions.RequiresHttps; + options.SaveTokens = true; + options.Scope.Add(Constants.PermissionsScope); + options.Scope.Add(Constants.ProfileScope); + options.Scope.Add(Constants.RoleScope); + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }); + } + + return authBuilder; + } + } +} diff --git a/backend/src/Squidex/Config/Authentication/IdentityServices.cs b/backend/src/Squidex/Config/Authentication/IdentityServices.cs new file mode 100644 index 000000000..43ad4a929 --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/IdentityServices.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Users; +using Squidex.Shared.Users; + +namespace Squidex.Config.Authentication +{ + public static class IdentityServices + { + public static void AddSquidexIdentity(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("identity")); + + services.AddSingletonAs() + .AsOptional(); + + services.AddSingletonAs() + .AsOptional(); + } + } +} diff --git a/backend/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs b/backend/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs new file mode 100644 index 000000000..0f03f68a0 --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; + +namespace Squidex.Config.Authentication +{ + public static class MicrosoftAuthenticationServices + { + public static AuthenticationBuilder AddSquidexExternalMicrosoftAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) + { + if (identityOptions.IsMicrosoftAuthConfigured()) + { + authBuilder.AddMicrosoftAccount(options => + { + options.ClientId = identityOptions.MicrosoftClient; + options.ClientSecret = identityOptions.MicrosoftSecret; + options.Events = new MicrosoftHandler(); + }); + } + + return authBuilder; + } + } +} diff --git a/backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs b/backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs new file mode 100644 index 000000000..718924871 --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OAuth; +using Squidex.Shared.Identity; + +namespace Squidex.Config.Authentication +{ + public sealed class MicrosoftHandler : OAuthEvents + { + public override Task CreatingTicket(OAuthCreatingTicketContext context) + { + string? displayName = null; + + if (context.User.TryGetProperty("displayName", out var element1) && element1.ValueKind == JsonValueKind.String) + { + displayName = element1.GetString(); + } + + if (!string.IsNullOrEmpty(displayName)) + { + context.Identity.SetDisplayName(displayName); + } + + string? id = null; + + if (context.User.TryGetProperty("id", out var element2) && element2.ValueKind == JsonValueKind.String) + { + id = element2.GetString(); + } + + if (!string.IsNullOrEmpty(id)) + { + var pictureUrl = $"https://apis.live.net/v5.0/{id}/picture"; + + context.Identity.SetPictureUrl(pictureUrl); + } + + return base.CreatingTicket(context); + } + } +} diff --git a/src/Squidex/Config/Authentication/OidcHandler.cs b/backend/src/Squidex/Config/Authentication/OidcHandler.cs similarity index 100% rename from src/Squidex/Config/Authentication/OidcHandler.cs rename to backend/src/Squidex/Config/Authentication/OidcHandler.cs diff --git a/backend/src/Squidex/Config/Authentication/OidcServices.cs b/backend/src/Squidex/Config/Authentication/OidcServices.cs new file mode 100644 index 000000000..00d82cbcc --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/OidcServices.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Extensions.DependencyInjection; + +namespace Squidex.Config.Authentication +{ + public static class OidcServices + { + public static AuthenticationBuilder AddSquidexExternalOdic(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) + { + if (identityOptions.IsOidcConfigured()) + { + var displayName = !string.IsNullOrWhiteSpace(identityOptions.OidcName) ? identityOptions.OidcName : OpenIdConnectDefaults.DisplayName; + + authBuilder.AddOpenIdConnect("ExternalOidc", displayName, options => + { + options.Authority = identityOptions.OidcAuthority; + options.ClientId = identityOptions.OidcClient; + options.ClientSecret = identityOptions.OidcSecret; + options.RequireHttpsMetadata = false; + options.Events = new OidcHandler(identityOptions); + + if (identityOptions.OidcScopes != null) + { + foreach (var scope in identityOptions.OidcScopes) + { + options.Scope.Add(scope); + } + } + }); + } + + return authBuilder; + } + } +} diff --git a/backend/src/Squidex/Config/Domain/AppsServices.cs b/backend/src/Squidex/Config/Domain/AppsServices.cs new file mode 100644 index 000000000..e7e7a6001 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/AppsServices.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Areas.Api.Controllers.UI; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.History; + +namespace Squidex.Config.Domain +{ + public static class AppsServices + { + public static void AddSquidexApps(this IServiceCollection services) + { + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingleton(c => + { + var uiOptions = c.GetRequiredService>().Value; + + var result = new InitialPatterns(); + + if (uiOptions.RegexSuggestions != null) + { + foreach (var (key, value) in uiOptions.RegexSuggestions) + { + if (!string.IsNullOrWhiteSpace(key) && + !string.IsNullOrWhiteSpace(value)) + { + result[Guid.NewGuid()] = new AppPattern(key, value); + } + } + } + + return result; + }); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs new file mode 100644 index 000000000..2e9251998 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -0,0 +1,130 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using FluentFTP; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using MongoDB.Driver.GridFS; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.Queries; +using Squidex.Domain.Apps.Entities.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Assets.ImageSharp; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; + +namespace Squidex.Config.Domain +{ + public static class AssetServices + { + public static void AddSquidexAssets(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("assets")); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As().As(); + + services.AddSingletonAs() + .As>(); + + services.AddSingletonAs() + .As>(); + } + + public static void AddSquidexAssetInfrastructure(this IServiceCollection services, IConfiguration config) + { + config.ConfigureByOption("assetStore:type", new Alternatives + { + ["Default"] = () => + { + services.AddSingletonAs() + .AsOptional(); + }, + ["Folder"] = () => + { + var path = config.GetRequiredValue("assetStore:folder:path"); + + services.AddSingletonAs(c => new FolderAssetStore(path, c.GetRequiredService())) + .As(); + }, + ["GoogleCloud"] = () => + { + var bucketName = config.GetRequiredValue("assetStore:googleCloud:bucket"); + + services.AddSingletonAs(c => new GoogleCloudAssetStore(bucketName)) + .As(); + }, + ["AzureBlob"] = () => + { + var connectionString = config.GetRequiredValue("assetStore:azureBlob:connectionString"); + var containerName = config.GetRequiredValue("assetStore:azureBlob:containerName"); + + services.AddSingletonAs(c => new AzureBlobAssetStore(connectionString, containerName)) + .As(); + }, + ["MongoDb"] = () => + { + var mongoConfiguration = config.GetRequiredValue("assetStore:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("assetStore:mongoDb:database"); + var mongoGridFsBucketName = config.GetRequiredValue("assetStore:mongoDb:bucket"); + + services.AddSingletonAs(c => + { + var mongoClient = Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s)); + var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); + + var gridFsbucket = new GridFSBucket(mongoDatabase, new GridFSBucketOptions + { + BucketName = mongoGridFsBucketName + }); + + return new MongoGridFsAssetStore(gridFsbucket); + }) + .As(); + }, + ["Ftp"] = () => + { + var serverHost = config.GetRequiredValue("assetStore:ftp:serverHost"); + var serverPort = config.GetOptionalValue("assetStore:ftp:serverPort", 21); + + var username = config.GetRequiredValue("assetStore:ftp:username"); + var password = config.GetRequiredValue("assetStore:ftp:password"); + + var path = config.GetOptionalValue("assetStore:ftp:path", "/"); + + services.AddSingletonAs(c => + { + var factory = new Func(() => new FtpClient(serverHost, serverPort, username, password)); + + return new FTPAssetStore(factory, path, c.GetRequiredService()); + }) + .As(); + } + }); + + services.AddSingletonAs() + .As(); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/BackupsServices.cs b/backend/src/Squidex/Config/Domain/BackupsServices.cs new file mode 100644 index 000000000..fe2edb927 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/BackupsServices.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Config.Domain +{ + public static class BackupsServices + { + public static void AddSquidexBackups(this IServiceCollection services) + { + services.AddSingletonAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs new file mode 100644 index 000000000..91526ff5d --- /dev/null +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -0,0 +1,114 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Indexes; +using Squidex.Domain.Apps.Entities.Apps.Invitation; +using Squidex.Domain.Apps.Entities.Apps.Templates; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Comments; +using Squidex.Domain.Apps.Entities.Comments.Commands; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.Rules.Indexes; +using Squidex.Domain.Apps.Entities.Rules.UsageTracking; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Domain.Apps.Entities.Schemas.Indexes; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Web.CommandMiddlewares; + +namespace Squidex.Config.Domain +{ + public static class CommandsServices + { + public static void AddSquidexCommands(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("mode")); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As().As(); + + services.AddSingletonAs() + .As().As(); + + services.AddSingletonAs() + .As().As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingleton(typeof(IEventEnricher<>), typeof(SquidexEventEnricher<>)); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/CommentsServices.cs b/backend/src/Squidex/Config/Domain/CommentsServices.cs new file mode 100644 index 000000000..2d3141a8c --- /dev/null +++ b/backend/src/Squidex/Config/Domain/CommentsServices.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities.Comments; + +namespace Squidex.Config.Domain +{ + public static class CommentsServices + { + public static void AddSquidexComments(this IServiceCollection services) + { + services.AddSingletonAs() + .As(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs b/backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs new file mode 100644 index 000000000..d546c4820 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace Squidex.Config.Domain +{ + public static class ConfigurationExtensions + { + public static void ConfigureForSquidex(this IConfigurationBuilder builder) + { + builder.AddJsonFile($"appsettings.Custom.json", true); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/ContentsServices.cs b/backend/src/Squidex/Config/Domain/ContentsServices.cs new file mode 100644 index 000000000..381f9e496 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/ContentsServices.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Queries; +using Squidex.Domain.Apps.Entities.Contents.Text; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Config.Domain +{ + public static class ContentsServices + { + public static void AddSquidexContents(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("contents")); + + services.AddSingletonAs(c => new Lazy(() => c.GetRequiredService())) + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsOptional(); + + services.AddSingletonAs() + .AsOptional(); + + services.AddSingletonAs() + .As().As(); + + services.AddSingletonAs>() + .AsSelf(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/EventPublishersServices.cs b/backend/src/Squidex/Config/Domain/EventPublishersServices.cs new file mode 100644 index 000000000..d1dfbd662 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/EventPublishersServices.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; + +namespace Squidex.Config.Domain +{ + public static class EventPublishersServices + { + public static void AddSquidexEventPublisher(this IServiceCollection services, IConfiguration config) + { + var eventPublishers = config.GetSection("eventPublishers"); + + foreach (var child in eventPublishers.GetChildren()) + { + var eventPublisherType = child.GetValue("type"); + + if (string.IsNullOrWhiteSpace(eventPublisherType)) + { + throw new ConfigurationException($"Configure EventPublisher type with 'eventPublishers:{child.Key}:type'."); + } + + var eventsFilter = child.GetValue("eventsFilter"); + + var enabled = child.GetValue("enabled"); + + if (string.Equals(eventPublisherType, "RabbitMq", StringComparison.OrdinalIgnoreCase)) + { + var publisherConfig = child.GetValue("configuration"); + + if (string.IsNullOrWhiteSpace(publisherConfig)) + { + throw new ConfigurationException($"Configure EventPublisher RabbitMq configuration with 'eventPublishers:{child.Key}:configuration'."); + } + + var exchange = child.GetValue("exchange"); + + if (string.IsNullOrWhiteSpace(exchange)) + { + throw new ConfigurationException($"Configure EventPublisher RabbitMq exchange with 'eventPublishers:{child.Key}:configuration'."); + } + + var name = $"EventPublishers_{child.Key}"; + + if (enabled) + { + services.AddSingletonAs(c => new RabbitMqEventConsumer(c.GetRequiredService(), name, publisherConfig, exchange, eventsFilter)) + .As(); + } + } + else + { + throw new ConfigurationException($"Unsupported value '{child.Key}' for 'eventPublishers:{child.Key}:type', supported: RabbitMq."); + } + } + } + } +} diff --git a/backend/src/Squidex/Config/Domain/EventSourcingServices.cs b/backend/src/Squidex/Config/Domain/EventSourcingServices.cs new file mode 100644 index 000000000..d35566e93 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/EventSourcingServices.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using EventStore.ClientAPI; +using Microsoft.Azure.Documents.Client; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Newtonsoft.Json; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Diagnostics; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.EventSourcing.Grains; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.States; + +namespace Squidex.Config.Domain +{ + public static class EventSourcingServices + { + public static void AddSquidexEventSourcing(this IServiceCollection services, IConfiguration config) + { + config.ConfigureByOption("eventStore:type", new Alternatives + { + ["MongoDb"] = () => + { + var mongoConfiguration = config.GetRequiredValue("eventStore:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("eventStore:mongoDb:database"); + + services.AddSingletonAs(c => + { + var mongoClient = Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s)); + var mongDatabase = mongoClient.GetDatabase(mongoDatabaseName); + + return new MongoEventStore(mongDatabase, c.GetRequiredService()); + }) + .As(); + }, + ["CosmosDb"] = () => + { + var cosmosDbConfiguration = config.GetRequiredValue("eventStore:cosmosDB:configuration"); + var cosmosDbMasterKey = config.GetRequiredValue("eventStore:cosmosDB:masterKey"); + var cosmosDbDatabase = config.GetRequiredValue("eventStore:cosmosDB:database"); + + services.AddSingletonAs(c => new DocumentClient(new Uri(cosmosDbConfiguration), cosmosDbMasterKey, c.GetRequiredService())) + .AsSelf(); + + services.AddSingletonAs(c => new CosmosDbEventStore( + c.GetRequiredService(), + cosmosDbMasterKey, + cosmosDbDatabase, + c.GetRequiredService())) + .As(); + + services.AddHealthChecks() + .AddCheck("CosmosDB", tags: new[] { "node" }); + }, + ["GetEventStore"] = () => + { + var eventStoreConfiguration = config.GetRequiredValue("eventStore:getEventStore:configuration"); + var eventStoreProjectionHost = config.GetRequiredValue("eventStore:getEventStore:projectionHost"); + var eventStorePrefix = config.GetValue("eventStore:getEventStore:prefix"); + + services.AddSingletonAs(_ => EventStoreConnection.Create(eventStoreConfiguration)) + .As(); + + services.AddSingletonAs(c => new GetEventStore( + c.GetRequiredService(), + c.GetRequiredService(), + eventStorePrefix, + eventStoreProjectionHost)) + .As(); + + services.AddHealthChecks() + .AddCheck("EventStore", tags: new[] { "node" }); + } + }); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs(c => + { + var allEventConsumers = c.GetServices(); + + return new EventConsumerFactory(n => allEventConsumers.FirstOrDefault(x => x.Name == n)); + }); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/HealthCheckServices.cs b/backend/src/Squidex/Config/Domain/HealthCheckServices.cs new file mode 100644 index 000000000..5e92bc0d5 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/HealthCheckServices.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities.Apps.Diagnostics; +using Squidex.Infrastructure.Diagnostics; + +namespace Squidex.Config.Domain +{ + public static class HealthCheckServices + { + public static void AddSquidexHealthChecks(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("healthz:gc")); + + services.AddHealthChecks() + .AddCheck("GC", tags: new[] { "node" }) + .AddCheck("Orleans", tags: new[] { "cluster" }) + .AddCheck("Orleans App", tags: new[] { "cluster" }); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/HistoryServices.cs b/backend/src/Squidex/Config/Domain/HistoryServices.cs new file mode 100644 index 000000000..3582407f5 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/HistoryServices.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Config.Domain +{ + public static class HistoryServices + { + public static void AddSquidexHistory(this IServiceCollection services) + { + services.AddSingletonAs() + .As().As(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs new file mode 100644 index 000000000..4c309c4a6 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Orleans; +using Squidex.Areas.Api.Controllers.Contents; +using Squidex.Areas.Api.Controllers.Contents.Generator; +using Squidex.Areas.Api.Controllers.News; +using Squidex.Areas.Api.Controllers.News.Service; +using Squidex.Areas.Api.Controllers.UI; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities.Rules.UsageTracking; +using Squidex.Domain.Apps.Entities.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing.Grains; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Translations; +using Squidex.Infrastructure.UsageTracking; +using Squidex.Pipeline.Robots; +using Squidex.Web; +using Squidex.Web.Pipeline; + +#pragma warning disable RECS0092 // Convert field to readonly + +namespace Squidex.Config.Domain +{ + public static class InfrastructureServices + { + public static void AddSquidexInfrastructure(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("urls")); + services.Configure( + config.GetSection("exposedConfiguration")); + + services.AddSingletonAs(_ => SystemClock.Instance) + .As(); + + services.AddSingletonAs>() + .AsSelf(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsOptional(); + + services.AddSingleton>(DomainObjectGrainFormatter.Format); + } + + public static void AddSquidexUsageTracking(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("usage")); + + services.AddSingletonAs(c => new CachingUsageTracker( + c.GetRequiredService(), + c.GetRequiredService())) + .As(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs>() + .AsSelf(); + } + + public static void AddSquidexTranslation(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("translations:deepL")); + services.Configure( + config.GetSection("languages")); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + } + + public static void AddSquidexControllerServices(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("robots")); + services.Configure( + config.GetSection("etags")); + services.Configure( + config.GetSection("contentsController")); + services.Configure( + config.GetSection("ui")); + services.Configure( + config.GetSection("news")); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/LoggingServices.cs b/backend/src/Squidex/Config/Domain/LoggingServices.cs new file mode 100644 index 000000000..8f7a1cdad --- /dev/null +++ b/backend/src/Squidex/Config/Domain/LoggingServices.cs @@ -0,0 +1,129 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Log.Adapter; +using Squidex.Web.Pipeline; + +namespace Squidex.Config.Domain +{ + public static class LoggingServices + { + public static void ConfigureForSquidex(this ILoggingBuilder builder, IConfiguration config) + { + builder.ClearProviders(); + + builder.AddConfiguration(config.GetSection("logging")); + + builder.AddSemanticLog(); + builder.AddFilters(); + + builder.Services.AddServices(config); + } + + private static void AddServices(this IServiceCollection services, IConfiguration config) + { + if (config.GetValue("logging:human")) + { + services.AddSingletonAs(_ => JsonLogWriterFactory.Readable()) + .As(); + } + else + { + services.AddSingletonAs(_ => JsonLogWriterFactory.Default()) + .As(); + } + + var loggingFile = config.GetValue("logging:file"); + + if (!string.IsNullOrWhiteSpace(loggingFile)) + { + services.AddSingletonAs(_ => new FileChannel(loggingFile)) + .As(); + } + + var useColors = config.GetValue("logging:colors"); + + services.AddSingletonAs(_ => new ConsoleLogChannel(useColors)) + .As(); + + services.AddSingletonAs(_ => new ApplicationInfoLogAppender(typeof(LoggingServices).Assembly, Guid.NewGuid())) + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsOptional(); + } + + private static void AddFilters(this ILoggingBuilder builder) + { + builder.AddFilter((category, level) => + { + if (level < LogLevel.Information) + { + return false; + } + + if (category.StartsWith("Orleans.Runtime.NoOpHostEnvironmentStatistics", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Error; + } + + if (category.StartsWith("Orleans.Runtime.SafeTimer", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Error; + } + + if (category.StartsWith("Orleans.Runtime.Scheduler", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Warning; + } + + if (category.StartsWith("Orleans.", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Warning; + } + + if (category.StartsWith("Runtime.", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Warning; + } + + if (category.StartsWith("Microsoft.AspNetCore.", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Warning; + } +#if LOG_ALL_IDENTITY_SERVER + if (category.StartsWith("IdentityServer4.", StringComparison.OrdinalIgnoreCase)) + { + return true; + } +#endif + return true; + }); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/MigrationServices.cs b/backend/src/Squidex/Config/Domain/MigrationServices.cs new file mode 100644 index 000000000..e70b38dcf --- /dev/null +++ b/backend/src/Squidex/Config/Domain/MigrationServices.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Migrate_01; +using Migrate_01.Migrations; +using Squidex.Infrastructure.Migrations; + +namespace Squidex.Config.Domain +{ + public static class MigrationServices + { + public static void AddSquidexMigration(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("rebuild")); + + services.AddSingletonAs() + .AsSelf(); + + services.AddTransientAs() + .AsSelf(); + + services.AddTransientAs() + .AsSelf(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/NotificationsServices.cs b/backend/src/Squidex/Config/Domain/NotificationsServices.cs new file mode 100644 index 000000000..951eaa4a1 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/NotificationsServices.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Entities.History.Notifications; +using Squidex.Infrastructure.Email; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Config.Domain +{ + public static class NotificationsServices + { + public static void AddSquidexNotifications(this IServiceCollection services, IConfiguration config) + { + var emailOptions = config.GetSection("email:smtp").Get(); + + if (emailOptions.IsConfigured()) + { + services.AddSingleton(Options.Create(emailOptions)); + + services.Configure( + config.GetSection("email:notifications")); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsOptional(); + } + else + { + services.AddSingletonAs() + .AsOptional(); + } + + services.AddSingletonAs() + .As(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/QueryServices.cs b/backend/src/Squidex/Config/Domain/QueryServices.cs new file mode 100644 index 000000000..f0c904be2 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/QueryServices.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL; +using GraphQL.DataLoader; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Infrastructure.Assets; +using Squidex.Web; +using Squidex.Web.Services; + +namespace Squidex.Config.Domain +{ + public static class QueryServices + { + public static void AddSquidexQueries(this IServiceCollection services, IConfiguration config) + { + var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true); + + services.AddSingletonAs(c => new UrlGenerator( + c.GetRequiredService>(), + c.GetRequiredService(), + exposeSourceUrl)) + .As().As().As().As(); + + services.AddSingletonAs(x => new FuncDependencyResolver(t => x.GetRequiredService(t))) + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs new file mode 100644 index 000000000..2cfc9c81a --- /dev/null +++ b/backend/src/Squidex/Config/Domain/RuleServices.cs @@ -0,0 +1,70 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Queries; +using Squidex.Domain.Apps.Entities.Rules.UsageTracking; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Config.Domain +{ + public static class RuleServices + { + public static void AddSquidexRules(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("rules")); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As().As(); + + services.AddSingletonAs() + .As().AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs>() + .AsSelf(); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/SchemasServices.cs b/backend/src/Squidex/Config/Domain/SchemasServices.cs new file mode 100644 index 000000000..9326df263 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/SchemasServices.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Config.Domain +{ + public static class SchemasServices + { + public static void AddSquidexSchemas(this IServiceCollection services) + { + services.AddSingletonAs() + .As(); + } + } +} \ No newline at end of file diff --git a/src/Squidex/Config/Domain/SerializationInitializer.cs b/backend/src/Squidex/Config/Domain/SerializationInitializer.cs similarity index 100% rename from src/Squidex/Config/Domain/SerializationInitializer.cs rename to backend/src/Squidex/Config/Domain/SerializationInitializer.cs diff --git a/backend/src/Squidex/Config/Domain/SerializationServices.cs b/backend/src/Squidex/Config/Domain/SerializationServices.cs new file mode 100644 index 000000000..dbd3e7c0d --- /dev/null +++ b/backend/src/Squidex/Config/Domain/SerializationServices.cs @@ -0,0 +1,124 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Migrate_01; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps.Json; +using Squidex.Domain.Apps.Core.Contents.Json; +using Squidex.Domain.Apps.Core.Rules.Json; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Schemas.Json; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Newtonsoft; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Config.Domain +{ + public static class SerializationServices + { + private static JsonSerializerSettings ConfigureJson(JsonSerializerSettings settings, TypeNameHandling typeNameHandling) + { + settings.Converters.Add(new StringEnumConverter()); + + settings.ContractResolver = new ConverterContractResolver( + new AppClientsConverter(), + new AppContributorsConverter(), + new AppPatternsConverter(), + new ClaimsPrincipalConverter(), + new ContentFieldDataConverter(), + new EnvelopeHeadersConverter(), + new FilterConverter(), + new InstantConverter(), + new JsonValueConverter(), + new LanguageConverter(), + new LanguagesConfigConverter(), + new NamedGuidIdConverter(), + new NamedLongIdConverter(), + new NamedStringIdConverter(), + new PropertyPathConverter(), + new RefTokenConverter(), + new RolesConverter(), + new RuleConverter(), + new SchemaConverter(), + new StatusConverter(), + new StringEnumConverter(), + new WorkflowConverter(), + new WorkflowTransitionConverter()); + + settings.NullValueHandling = NullValueHandling.Ignore; + + settings.DateFormatHandling = DateFormatHandling.IsoDateFormat; + settings.DateParseHandling = DateParseHandling.None; + + settings.TypeNameHandling = typeNameHandling; + + return settings; + } + + public static IServiceCollection AddSquidexSerializers(this IServiceCollection services) + { + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs(c => JsonSerializer.Create(c.GetRequiredService())) + .AsSelf(); + + services.AddSingletonAs(c => + { + var serializerSettings = ConfigureJson(new JsonSerializerSettings(), TypeNameHandling.Auto); + + var typeNameRegistry = c.GetService(); + + if (typeNameRegistry != null) + { + serializerSettings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); + } + + return serializerSettings; + }).As(); + + return services; + } + + public static IMvcBuilder AddSquidexSerializers(this IMvcBuilder mvc) + { + mvc.AddNewtonsoftJson(options => + { + ConfigureJson(options.SerializerSettings, TypeNameHandling.None); + }); + + return mvc; + } + } +} diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs new file mode 100644 index 000000000..c216fd390 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -0,0 +1,126 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using IdentityServer4.Stores; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Migrate_01.Migrations.MongoDb; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Entities.Contents.Text; +using Squidex.Domain.Apps.Entities.History.Repositories; +using Squidex.Domain.Apps.Entities.MongoDb.Assets; +using Squidex.Domain.Apps.Entities.MongoDb.Contents; +using Squidex.Domain.Apps.Entities.MongoDb.History; +using Squidex.Domain.Apps.Entities.MongoDb.Rules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Domain.Users; +using Squidex.Domain.Users.MongoDb; +using Squidex.Domain.Users.MongoDb.Infrastructure; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Diagnostics; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.UsageTracking; + +namespace Squidex.Config.Domain +{ + public static class StoreServices + { + public static void AddSquidexStoreServices(this IServiceCollection services, IConfiguration config) + { + config.ConfigureByOption("store:type", new Alternatives + { + ["MongoDB"] = () => + { + var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); + var mongoContentDatabaseName = config.GetOptionalValue("store:mongoDb:contentDatabase", mongoDatabaseName); + + services.AddSingleton(typeof(ISnapshotStore<,>), typeof(MongoSnapshotStore<,>)); + + services.AddSingletonAs(_ => Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s))) + .As(); + + services.AddSingletonAs(c => c.GetRequiredService().GetDatabase(mongoDatabaseName)) + .As(); + + services.AddTransientAs(c => new DeleteContentCollections(c.GetRequiredService().GetDatabase(mongoContentDatabaseName))) + .As(); + + services.AddTransientAs(c => new RestructureContentCollection(c.GetRequiredService().GetDatabase(mongoContentDatabaseName))) + .As(); + + services.AddSingletonAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddHealthChecks() + .AddCheck("MongoDB", tags: new[] { "node" }); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As>(); + + services.AddSingletonAs() + .As>() + .As(); + + services.AddSingletonAs() + .As() + .As>(); + + services.AddSingletonAs(c => new MongoContentRepository( + c.GetRequiredService().GetDatabase(mongoContentDatabaseName), + c.GetRequiredService(), + c.GetRequiredService(), + c.GetRequiredService(), + c.GetRequiredService())) + .As() + .As>() + .As(); + + var registration = services.FirstOrDefault(x => x.ServiceType == typeof(IPersistedGrantStore)); + + if (registration == null || registration.ImplementationType == typeof(InMemoryPersistedGrantStore)) + { + services.AddSingletonAs() + .As(); + } + } + }); + + services.AddSingleton(typeof(IStore<>), typeof(Store<>)); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/SubscriptionServices.cs b/backend/src/Squidex/Config/Domain/SubscriptionServices.cs new file mode 100644 index 000000000..bfda6b0da --- /dev/null +++ b/backend/src/Squidex/Config/Domain/SubscriptionServices.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; +using Squidex.Domain.Users; +using Squidex.Infrastructure; +using Squidex.Web; + +namespace Squidex.Config.Domain +{ + public static class SubscriptionServices + { + public static void AddSquidexSubscriptions(this IServiceCollection services, IConfiguration config) + { + services.AddSingletonAs(c => c.GetRequiredService>()?.Value?.Plans.OrEmpty()!); + + services.AddSingletonAs() + .AsOptional(); + + services.AddSingletonAs() + .AsOptional(); + + services.AddSingletonAs() + .AsOptional(); + } + } +} diff --git a/src/Squidex/Config/MyIdentityOptions.cs b/backend/src/Squidex/Config/MyIdentityOptions.cs similarity index 100% rename from src/Squidex/Config/MyIdentityOptions.cs rename to backend/src/Squidex/Config/MyIdentityOptions.cs diff --git a/src/Squidex/Config/Orleans/Helper.cs b/backend/src/Squidex/Config/Orleans/Helper.cs similarity index 100% rename from src/Squidex/Config/Orleans/Helper.cs rename to backend/src/Squidex/Config/Orleans/Helper.cs diff --git a/backend/src/Squidex/Config/Orleans/OrleansServices.cs b/backend/src/Squidex/Config/Orleans/OrleansServices.cs new file mode 100644 index 000000000..7f320a676 --- /dev/null +++ b/backend/src/Squidex/Config/Orleans/OrleansServices.cs @@ -0,0 +1,125 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Orleans; +using Orleans.Configuration; +using Orleans.Hosting; +using Orleans.Providers.MongoDB.Configuration; +using OrleansDashboard; +using Squidex.Domain.Apps.Entities; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Squidex.Web; + +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Squidex.Config.Orleans +{ + public static class OrleansServices + { + public static void ConfigureForSquidex(this ISiloBuilder builder, IConfiguration config) + { + builder.ConfigureServices(siloServices => + { + siloServices.AddSingleton(); + siloServices.AddScoped(); + + siloServices.AddScoped(typeof(IGrainState<>), typeof(Squidex.Infrastructure.Orleans.GrainState<>)); + }); + + builder.ConfigureApplicationParts(parts => + { + parts.AddApplicationPart(SquidexEntities.Assembly); + parts.AddApplicationPart(SquidexInfrastructure.Assembly); + }); + + builder.Configure(options => + { + options.Configure(); + }); + + builder.Configure(options => + { + options.FastKillOnProcessExit = false; + }); + + builder.Configure(options => + { + options.HideTrace = true; + }); + + builder.UseDashboard(options => + { + options.HostSelf = false; + }); + + builder.AddIncomingGrainCallFilter(); + builder.AddIncomingGrainCallFilter(); + builder.AddIncomingGrainCallFilter(); + builder.AddIncomingGrainCallFilter(); + + var orleansPortSilo = config.GetOptionalValue("orleans:siloPort", 11111); + var orleansPortGateway = config.GetOptionalValue("orleans:gatewayPort", 40000); + + var address = Helper.ResolveIPAddressAsync(Dns.GetHostName(), AddressFamily.InterNetwork).Result; + + builder.ConfigureEndpoints( + address, + orleansPortSilo, + orleansPortGateway, + true); + + config.ConfigureByOption("orleans:clustering", new Alternatives + { + ["MongoDB"] = () => + { + builder.UseMongoDBClustering(options => + { + options.Configure(config); + }); + }, + ["Development"] = () => + { + builder.UseDevelopmentClustering(new IPEndPoint(address, orleansPortSilo)); + } + }); + + config.ConfigureByOption("store:type", new Alternatives + { + ["MongoDB"] = () => + { + builder.UseMongoDBReminders(options => + { + options.Configure(config); + }); + } + }); + } + + private static void Configure(this MongoDBOptions options, IConfiguration config) + { + var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); + + options.ConnectionString = mongoConfiguration; + options.CollectionPrefix = "Orleans_"; + + options.DatabaseName = mongoDatabaseName; + } + + private static void Configure(this ClusterOptions options) + { + options.ClusterId = Constants.OrleansClusterId; + options.ServiceId = Constants.OrleansClusterId; + } + } +} diff --git a/backend/src/Squidex/Config/Startup/BackgroundHost.cs b/backend/src/Squidex/Config/Startup/BackgroundHost.cs new file mode 100644 index 000000000..deb6c41fe --- /dev/null +++ b/backend/src/Squidex/Config/Startup/BackgroundHost.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. + +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Config.Startup +{ + public sealed class BackgroundHost : SafeHostedService + { + private readonly IEnumerable targets; + + public BackgroundHost(IEnumerable targets, ISemanticLog log) + : base(log) + { + this.targets = targets; + } + + protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) + { + foreach (var target in targets.Distinct()) + { + await target.StartAsync(ct); + + log.LogInformation(w => w.WriteProperty("backgroundSystem", target.ToString())); + } + } + } +} diff --git a/backend/src/Squidex/Config/Startup/InitializerHost.cs b/backend/src/Squidex/Config/Startup/InitializerHost.cs new file mode 100644 index 000000000..812fd0f19 --- /dev/null +++ b/backend/src/Squidex/Config/Startup/InitializerHost.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Config.Startup +{ + public sealed class InitializerHost : SafeHostedService + { + private readonly IEnumerable targets; + + public InitializerHost(IEnumerable targets, ISemanticLog log) + : base(log) + { + this.targets = targets; + } + + protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) + { + foreach (var target in targets.Distinct()) + { + await target.InitializeAsync(ct); + + log.LogInformation(w => w.WriteProperty("initializedSystem", target.GetType().Name)); + } + } + } +} diff --git a/backend/src/Squidex/Config/Startup/LogConfigurationHost.cs b/backend/src/Squidex/Config/Startup/LogConfigurationHost.cs new file mode 100644 index 000000000..6996f1435 --- /dev/null +++ b/backend/src/Squidex/Config/Startup/LogConfigurationHost.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Config.Startup +{ + public sealed class LogConfigurationHost : SafeHostedService + { + private readonly IConfiguration configuration; + + public LogConfigurationHost(ISemanticLog log, IConfiguration configuration) + : base(log) + { + this.configuration = configuration; + } + + protected override Task StartAsync(ISemanticLog log, CancellationToken ct) + { + log.LogInformation(w => w + .WriteProperty("message", "Application started") + .WriteObject("environment", c => + { + var logged = new HashSet(StringComparer.OrdinalIgnoreCase); + + var orderedConfigs = configuration.AsEnumerable().Where(kvp => kvp.Value != null).OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase); + + foreach (var (key, val) in orderedConfigs) + { + if (logged.Add(key)) + { + c.WriteProperty(key.ToLowerInvariant(), val); + } + } + })); + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs b/backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs new file mode 100644 index 000000000..5523ca278 --- /dev/null +++ b/backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Migrate_01; +using Squidex.Infrastructure.Log; + +namespace Squidex.Config.Startup +{ + public sealed class MigrationRebuilderHost : SafeHostedService + { + private readonly RebuildRunner rebuildRunner; + + public MigrationRebuilderHost(RebuildRunner rebuildRunner, ISemanticLog log) + : base(log) + { + this.rebuildRunner = rebuildRunner; + } + + protected override Task StartAsync(ISemanticLog log, CancellationToken ct) + { + return rebuildRunner.RunAsync(ct); + } + } +} diff --git a/backend/src/Squidex/Config/Startup/MigratorHost.cs b/backend/src/Squidex/Config/Startup/MigratorHost.cs new file mode 100644 index 000000000..29fcf7cb1 --- /dev/null +++ b/backend/src/Squidex/Config/Startup/MigratorHost.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Migrations; + +namespace Squidex.Config.Startup +{ + public sealed class MigratorHost : SafeHostedService + { + private readonly Migrator migrator; + + public MigratorHost(Migrator migrator, ISemanticLog log) + : base(log) + { + this.migrator = migrator; + } + + protected override Task StartAsync(ISemanticLog log, CancellationToken ct) + { + return migrator.MigrateAsync(ct); + } + } +} diff --git a/backend/src/Squidex/Config/Startup/SafeHostedService.cs b/backend/src/Squidex/Config/Startup/SafeHostedService.cs new file mode 100644 index 000000000..286d22dbf --- /dev/null +++ b/backend/src/Squidex/Config/Startup/SafeHostedService.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Config.Startup +{ + public abstract class SafeHostedService : IHostedService + { + private readonly ISemanticLog log; + private bool isStarted; + + protected SafeHostedService(ISemanticLog log) + { + this.log = log; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + await StartAsync(log, cancellationToken); + + isStarted = true; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (isStarted) + { + await StopAsync(log, cancellationToken); + } + } + + protected abstract Task StartAsync(ISemanticLog log, CancellationToken ct); + + protected virtual Task StopAsync(ISemanticLog log, CancellationToken ct) + { + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex/Config/Web/WebExtensions.cs b/backend/src/Squidex/Config/Web/WebExtensions.cs new file mode 100644 index 000000000..3bc5022cf --- /dev/null +++ b/backend/src/Squidex/Config/Web/WebExtensions.cs @@ -0,0 +1,121 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Net.Http.Headers; +using Squidex.Infrastructure.Json; +using Squidex.Pipeline.Robots; +using Squidex.Web.Pipeline; + +namespace Squidex.Config.Web +{ + public static class WebExtensions + { + public static IApplicationBuilder UseSquidexLocalCache(this IApplicationBuilder app) + { + app.UseMiddleware(); + + return app; + } + + public static IApplicationBuilder UseSquidexTracking(this IApplicationBuilder app) + { + app.UseMiddleware(); + + return app; + } + + public static IApplicationBuilder UseSquidexHealthCheck(this IApplicationBuilder app) + { + var serializer = app.ApplicationServices.GetRequiredService(); + + var writer = new Func((httpContext, report) => + { + var response = new + { + Entries = report.Entries.ToDictionary(x => x.Key, x => + { + var value = x.Value; + + return new + { + Data = value.Data.Count > 0 ? new Dictionary(value.Data) : null, + value.Description, + value.Duration, + value.Status + }; + }), + report.Status, + report.TotalDuration + }; + + var json = serializer.Serialize(response); + + httpContext.Response.Headers[HeaderNames.ContentType] = "text/json"; + + return httpContext.Response.WriteAsync(json); + }); + + app.UseHealthChecks("/readiness", new HealthCheckOptions + { + Predicate = check => true, + ResponseWriter = writer + }); + + app.UseHealthChecks("/healthz", new HealthCheckOptions + { + Predicate = check => check.Tags.Contains("node"), + ResponseWriter = writer + }); + + app.UseHealthChecks("/cluster-healthz", new HealthCheckOptions + { + Predicate = check => check.Tags.Contains("cluster"), + ResponseWriter = writer + }); + + return app; + } + + public static IApplicationBuilder UseSquidexRobotsTxt(this IApplicationBuilder app) + { + app.Map("/robots.txt", builder => builder.UseMiddleware()); + + return app; + } + + public static void UseSquidexCors(this IApplicationBuilder app) + { + app.UseCors(builder => builder + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + } + + public static void UseSquidexForwardingRules(this IApplicationBuilder app) + { + app.UseForwardedHeaders(new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedProto, + ForwardLimit = null, + RequireHeaderSymmetry = false + }); + + app.UseMiddleware(); + app.UseMiddleware(); + } + } +} diff --git a/backend/src/Squidex/Config/Web/WebServices.cs b/backend/src/Squidex/Config/Web/WebServices.cs new file mode 100644 index 000000000..b6a8b74d7 --- /dev/null +++ b/backend/src/Squidex/Config/Web/WebServices.cs @@ -0,0 +1,76 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Config.Domain; +using Squidex.Domain.Apps.Entities; +using Squidex.Pipeline.Plugins; +using Squidex.Pipeline.Robots; +using Squidex.Web; +using Squidex.Web.Pipeline; + +namespace Squidex.Config.Web +{ + public static class WebServices + { + public static void AddSquidexMvcWithPlugins(this IServiceCollection services, IConfiguration config) + { + services.AddSingletonAs(c => new ExposedValues(c.GetRequiredService>().Value, config, typeof(WebServices).Assembly)) + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.Configure(options => + { + options.SuppressModelStateInvalidFilter = true; + }); + + services.AddMvc(options => + { + options.Filters.Add(); + options.Filters.Add(); + options.Filters.Add(); + options.Filters.Add(); + }) + .AddSquidexPlugins(config) + .AddSquidexSerializers(); + } + } +} diff --git a/src/Squidex/Docs/schemabody.md b/backend/src/Squidex/Docs/schemabody.md similarity index 100% rename from src/Squidex/Docs/schemabody.md rename to backend/src/Squidex/Docs/schemabody.md diff --git a/src/Squidex/Docs/schemaquery.md b/backend/src/Squidex/Docs/schemaquery.md similarity index 100% rename from src/Squidex/Docs/schemaquery.md rename to backend/src/Squidex/Docs/schemaquery.md diff --git a/src/Squidex/Docs/security.md b/backend/src/Squidex/Docs/security.md similarity index 100% rename from src/Squidex/Docs/security.md rename to backend/src/Squidex/Docs/security.md diff --git a/backend/src/Squidex/Pipeline/OpenApi/NSwagHelper.cs b/backend/src/Squidex/Pipeline/OpenApi/NSwagHelper.cs new file mode 100644 index 000000000..140bb62d1 --- /dev/null +++ b/backend/src/Squidex/Pipeline/OpenApi/NSwagHelper.cs @@ -0,0 +1,114 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.AspNetCore.Http; +using NJsonSchema; +using NSwag; + +namespace Squidex.Pipeline.OpenApi +{ + public static class NSwagHelper + { + public static readonly string SecurityDocs = LoadDocs("security"); + + public static readonly string SchemaBodyDocs = LoadDocs("schemabody"); + + public static readonly string SchemaQueryDocs = LoadDocs("schemaquery"); + + private static string LoadDocs(string name) + { + var assembly = typeof(NSwagHelper).Assembly; + + using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Docs.{name}.md")) + { + using (var streamReader = new StreamReader(resourceStream!)) + { + return streamReader.ReadToEnd(); + } + } + } + + public static OpenApiDocument CreateApiDocument(HttpContext context, string appName) + { + var scheme = + string.Equals(context.Request.Scheme, "http", StringComparison.OrdinalIgnoreCase) ? + OpenApiSchema.Http : + OpenApiSchema.Https; + + var document = new OpenApiDocument + { + Schemes = new List + { + scheme + }, + Consumes = new List + { + "application/json" + }, + Produces = new List + { + "application/json" + }, + Info = new OpenApiInfo + { + Title = $"Squidex API for {appName} App" + }, + SchemaType = SchemaType.OpenApi3 + }; + + if (!string.IsNullOrWhiteSpace(context.Request.Host.Value)) + { + document.Host = context.Request.Host.Value; + } + + return document; + } + + public static void AddQuery(this OpenApiOperation operation, string name, JsonObjectType type, string description) + { + var schema = new JsonSchema { Type = type }; + + operation.AddParameter(name, schema, OpenApiParameterKind.Query, description, false); + } + + public static void AddPathParameter(this OpenApiOperation operation, string name, JsonObjectType type, string description, string? format = null) + { + var schema = new JsonSchema { Type = type, Format = format }; + + operation.AddParameter(name, schema, OpenApiParameterKind.Path, description, true); + } + + public static void AddBody(this OpenApiOperation operation, string name, JsonSchema schema, string description) + { + operation.AddParameter(name, schema, OpenApiParameterKind.Body, description, true); + } + + private static void AddParameter(this OpenApiOperation operation, string name, JsonSchema schema, OpenApiParameterKind kind, string description, bool isRequired) + { + var parameter = new OpenApiParameter { Schema = schema, Name = name, Kind = kind }; + + if (!string.IsNullOrWhiteSpace(description)) + { + parameter.Description = description; + } + + parameter.IsRequired = isRequired; + + operation.Parameters.Add(parameter); + } + + public static void AddResponse(this OpenApiOperation operation, string statusCode, string description, JsonSchema? schema = null) + { + var response = new OpenApiResponse { Description = description, Schema = schema }; + + operation.Responses.Add(statusCode, response); + } + } +} diff --git a/src/Squidex/Pipeline/Plugins/MvcParts.cs b/backend/src/Squidex/Pipeline/Plugins/MvcParts.cs similarity index 100% rename from src/Squidex/Pipeline/Plugins/MvcParts.cs rename to backend/src/Squidex/Pipeline/Plugins/MvcParts.cs diff --git a/backend/src/Squidex/Pipeline/Plugins/PluginExtensions.cs b/backend/src/Squidex/Pipeline/Plugins/PluginExtensions.cs new file mode 100644 index 000000000..cab52c7fa --- /dev/null +++ b/backend/src/Squidex/Pipeline/Plugins/PluginExtensions.cs @@ -0,0 +1,81 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Plugins; + +namespace Squidex.Pipeline.Plugins +{ + public static class PluginExtensions + { + public static IMvcBuilder AddSquidexPlugins(this IMvcBuilder mvcBuilder, IConfiguration config) + { + var pluginManager = new PluginManager(); + + var options = config.Get(); + + if (options.Plugins != null) + { + foreach (var path in options.Plugins) + { + var plugin = PluginLoaders.LoadPlugin(path); + + if (plugin != null) + { + try + { + var pluginAssembly = plugin.LoadDefaultAssembly(); + + pluginAssembly.AddParts(mvcBuilder); + pluginManager.Add(path, pluginAssembly); + } + catch (Exception ex) + { + pluginManager.LogException(path, "LoadingAssembly", ex); + } + } + else + { + pluginManager.LogException(path, "LoadingPlugin", new FileNotFoundException($"Cannot find plugin at {path}")); + } + } + } + + pluginManager.ConfigureServices(mvcBuilder.Services, config); + + mvcBuilder.Services.AddSingleton(pluginManager); + + return mvcBuilder; + } + + public static void UsePluginsBefore(this IApplicationBuilder app) + { + var pluginManager = app.ApplicationServices.GetRequiredService(); + + pluginManager.ConfigureBefore(app); + } + + public static void UsePluginsAfter(this IApplicationBuilder app) + { + var pluginManager = app.ApplicationServices.GetRequiredService(); + + pluginManager.ConfigureAfter(app); + } + + public static void UsePlugins(this IApplicationBuilder app) + { + var pluginManager = app.ApplicationServices.GetRequiredService(); + + pluginManager.Log(app.ApplicationServices.GetService()); + } + } +} diff --git a/backend/src/Squidex/Pipeline/Plugins/PluginLoaders.cs b/backend/src/Squidex/Pipeline/Plugins/PluginLoaders.cs new file mode 100644 index 000000000..d2471144f --- /dev/null +++ b/backend/src/Squidex/Pipeline/Plugins/PluginLoaders.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using McMaster.NETCore.Plugins; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Plugins; +using Squidex.Web; + +namespace Squidex.Pipeline.Plugins +{ + public static class PluginLoaders + { + private static readonly Type[] SharedTypes = + { + typeof(IPlugin), + typeof(SquidexCoreModel), + typeof(SquidexCoreOperations), + typeof(SquidexEntities), + typeof(SquidexEvents), + typeof(SquidexInfrastructure), + typeof(SquidexWeb) + }; + + public static PluginLoader? LoadPlugin(string pluginPath) + { + foreach (var candidate in GetPaths(pluginPath)) + { + if (candidate.Extension.Equals(".dll", StringComparison.OrdinalIgnoreCase)) + { + return PluginLoader.CreateFromAssemblyFile(candidate.FullName, config => + { + config.PreferSharedTypes = true; + + config.SharedAssemblies.AddRange(SharedTypes.Select(x => x.Assembly.GetName())); + }); + } + } + + return null; + } + + private static IEnumerable GetPaths(string pluginPath) + { + var candidate = new FileInfo(Path.GetFullPath(pluginPath)); + + if (candidate.Exists) + { + yield return candidate; + } + + if (!Path.IsPathRooted(pluginPath)) + { + var assembly = Assembly.GetEntryAssembly(); + + if (assembly != null) + { + var directory = Path.GetDirectoryName(assembly.Location); + + if (directory != null) + { + candidate = new FileInfo(Path.Combine(directory, pluginPath)); + + if (candidate.Exists) + { + yield return candidate; + } + } + } + } + } + } +} diff --git a/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs b/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs new file mode 100644 index 000000000..a75e32ded --- /dev/null +++ b/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Squidex.Infrastructure; + +namespace Squidex.Pipeline.Robots +{ + public sealed class RobotsTxtMiddleware : IMiddleware + { + private readonly RobotsTxtOptions robotsTxtOptions; + + public RobotsTxtMiddleware(IOptions robotsTxtOptions) + { + Guard.NotNull(robotsTxtOptions); + + this.robotsTxtOptions = robotsTxtOptions.Value; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + if (CanServeRequest(context.Request) && !string.IsNullOrWhiteSpace(robotsTxtOptions.Text)) + { + context.Response.ContentType = "text/plain"; + context.Response.StatusCode = 200; + + await context.Response.WriteAsync(robotsTxtOptions.Text); + } + else + { + await next(context); + } + } + + private static bool CanServeRequest(HttpRequest request) + { + return HttpMethods.IsGet(request.Method) && string.IsNullOrEmpty(request.Path); + } + } +} diff --git a/src/Squidex/Pipeline/Robots/RobotsTxtOptions.cs b/backend/src/Squidex/Pipeline/Robots/RobotsTxtOptions.cs similarity index 100% rename from src/Squidex/Pipeline/Robots/RobotsTxtOptions.cs rename to backend/src/Squidex/Pipeline/Robots/RobotsTxtOptions.cs diff --git a/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs b/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs new file mode 100644 index 000000000..da212d8c0 --- /dev/null +++ b/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs @@ -0,0 +1,144 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Squidex.Pipeline.Squid +{ + public sealed class SquidMiddleware + { + private readonly RequestDelegate next; + private readonly string squidHappyLG = LoadSvg("happy"); + private readonly string squidHappySM = LoadSvg("happy-sm"); + private readonly string squidSadLG = LoadSvg("sad"); + private readonly string squidSadSM = LoadSvg("sad-sm"); + + public SquidMiddleware(RequestDelegate next) + { + this.next = next; + } + + public async Task Invoke(HttpContext context) + { + var request = context.Request; + + if (request.Path.Equals("/squid.svg")) + { + var face = "sad"; + + if (request.Query.TryGetValue("face", out var faceValue) && (faceValue == "sad" || faceValue == "happy")) + { + face = faceValue; + } + + var isSad = face == "sad"; + + var title = isSad ? "OH DAMN!" : "OH YEAH!"; + + if (request.Query.TryGetValue("title", out var titleValue) && !string.IsNullOrWhiteSpace(titleValue)) + { + title = titleValue; + } + + var text = "text"; + + if (request.Query.TryGetValue("text", out var textValue) && !string.IsNullOrWhiteSpace(textValue)) + { + text = textValue; + } + + var background = isSad ? "#F5F5F9" : "#4CC159"; + + if (request.Query.TryGetValue("background", out var backgroundValue) && !string.IsNullOrWhiteSpace(backgroundValue)) + { + background = backgroundValue; + } + + var isSmall = request.Query.TryGetValue("small", out _); + + string svg; + + if (isSmall) + { + svg = isSad ? squidSadSM : squidHappySM; + } + else + { + svg = isSad ? squidSadLG : squidHappyLG; + } + + var (l1, l2, l3) = SplitText(text); + + svg = svg.Replace("{{TITLE}}", title.ToUpperInvariant()); + svg = svg.Replace("{{TEXT1}}", l1); + svg = svg.Replace("{{TEXT2}}", l2); + svg = svg.Replace("{{TEXT3}}", l3); + svg = svg.Replace("[COLOR]", background); + + context.Response.StatusCode = 200; + context.Response.ContentType = "image/svg+xml"; + context.Response.Headers["Cache-Control"] = "public, max-age=604800"; + + await context.Response.WriteAsync(svg); + } + else + { + await next(context); + } + } + + private static (string, string, string) SplitText(string text) + { + var result = new List(); + + var line = new StringBuilder(); + + foreach (var word in text.Split(' ')) + { + if (line.Length + word.Length > 17 && line.Length > 0) + { + result.Add(line.ToString()); + + line.Clear(); + } + + if (line.Length > 0) + { + line.Append(" "); + } + + line.Append(word); + } + + result.Add(line.ToString()); + + while (result.Count < 3) + { + result.Add(string.Empty); + } + + return (result[0], result[1], result[2]); + } + + private static string LoadSvg(string name) + { + var assembly = typeof(SquidMiddleware).Assembly; + + using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Pipeline.Squid.icon-{name}.svg")) + { + using (var streamReader = new StreamReader(resourceStream!)) + { + return streamReader.ReadToEnd(); + } + } + } + } +} diff --git a/src/Squidex/Pipeline/Squid/icon-happy-sm.svg b/backend/src/Squidex/Pipeline/Squid/icon-happy-sm.svg similarity index 100% rename from src/Squidex/Pipeline/Squid/icon-happy-sm.svg rename to backend/src/Squidex/Pipeline/Squid/icon-happy-sm.svg diff --git a/src/Squidex/Pipeline/Squid/icon-happy.svg b/backend/src/Squidex/Pipeline/Squid/icon-happy.svg similarity index 100% rename from src/Squidex/Pipeline/Squid/icon-happy.svg rename to backend/src/Squidex/Pipeline/Squid/icon-happy.svg diff --git a/src/Squidex/Pipeline/Squid/icon-sad-sm.svg b/backend/src/Squidex/Pipeline/Squid/icon-sad-sm.svg similarity index 100% rename from src/Squidex/Pipeline/Squid/icon-sad-sm.svg rename to backend/src/Squidex/Pipeline/Squid/icon-sad-sm.svg diff --git a/src/Squidex/Pipeline/Squid/icon-sad.svg b/backend/src/Squidex/Pipeline/Squid/icon-sad.svg similarity index 100% rename from src/Squidex/Pipeline/Squid/icon-sad.svg rename to backend/src/Squidex/Pipeline/Squid/icon-sad.svg diff --git a/backend/src/Squidex/Program.cs b/backend/src/Squidex/Program.cs new file mode 100644 index 000000000..791e13cbb --- /dev/null +++ b/backend/src/Squidex/Program.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Squidex.Areas.IdentityServer.Config; +using Squidex.Config.Domain; +using Squidex.Config.Orleans; +using Squidex.Config.Startup; + +namespace Squidex +{ + public static class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureLogging((context, builder) => + { + builder.ConfigureForSquidex(context.Configuration); + }) + .ConfigureAppConfiguration((hostContext, builder) => + { + builder.ConfigureForSquidex(); + }) + .ConfigureServices(services => + { + // Step 0: Log all configuration. + services.AddHostedService(); + + // Step 1: Initialize all services. + services.AddHostedService(); + + // Step 2: Create admin user. + services.AddHostedService(); + }) + .UseOrleans((context, builder) => + { + // Step 3: Start Orleans. + builder.ConfigureForSquidex(context.Configuration); + }) + .ConfigureServices(services => + { + // Step 4: Run migration. + services.AddHostedService(); + + // Step 5: Run rebuild processes. + services.AddHostedService(); + + // Step 6: Start background processes. + services.AddHostedService(); + }) + .ConfigureWebHostDefaults(builder => + { + builder.UseStartup(); + }); + } +} diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj new file mode 100644 index 000000000..3ab06a94a --- /dev/null +++ b/backend/src/Squidex/Squidex.csproj @@ -0,0 +1,130 @@ + + + netcoreapp3.0 + Latest + true + 8.0 + enable + + + + full + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_DocumentationFile Include="$(DocumentationFile)" /> + + + + + + true + + + + ..\..\Squidex.ruleset + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(NoWarn);CS1591;1591;1573;1572;NU1605;IDE0060 + + \ No newline at end of file diff --git a/backend/src/Squidex/Startup.cs b/backend/src/Squidex/Startup.cs new file mode 100644 index 000000000..1e9cb14d7 --- /dev/null +++ b/backend/src/Squidex/Startup.cs @@ -0,0 +1,93 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Areas.Api; +using Squidex.Areas.Api.Config.OpenApi; +using Squidex.Areas.Frontend; +using Squidex.Areas.IdentityServer; +using Squidex.Areas.IdentityServer.Config; +using Squidex.Areas.OrleansDashboard; +using Squidex.Areas.Portal; +using Squidex.Config.Authentication; +using Squidex.Config.Domain; +using Squidex.Config.Web; +using Squidex.Pipeline.Plugins; + +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Squidex +{ + public sealed class Startup + { + private readonly IConfiguration config; + + public Startup(IConfiguration config) + { + this.config = config; + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddHttpClient(); + services.AddMemoryCache(); + + services.AddSquidexMvcWithPlugins(config); + + services.AddSquidexApps(); + services.AddSquidexAssetInfrastructure(config); + services.AddSquidexAssets(config); + services.AddSquidexAuthentication(config); + services.AddSquidexBackups(); + services.AddSquidexCommands(config); + services.AddSquidexComments(); + services.AddSquidexContents(config); + services.AddSquidexControllerServices(config); + services.AddSquidexEventPublisher(config); + services.AddSquidexEventSourcing(config); + services.AddSquidexHealthChecks(config); + services.AddSquidexHistory(); + services.AddSquidexIdentity(config); + services.AddSquidexIdentityServer(); + services.AddSquidexInfrastructure(config); + services.AddSquidexMigration(config); + services.AddSquidexNotifications(config); + services.AddSquidexOpenApiSettings(); + services.AddSquidexQueries(config); + services.AddSquidexRules(config); + services.AddSquidexSchemas(); + services.AddSquidexSerializers(); + services.AddSquidexStoreServices(config); + services.AddSquidexSubscriptions(config); + services.AddSquidexTranslation(config); + services.AddSquidexUsageTracking(config); + } + + public void Configure(IApplicationBuilder app) + { + app.UsePluginsBefore(); + + app.UseSquidexHealthCheck(); + app.UseSquidexRobotsTxt(); + app.UseSquidexTracking(); + app.UseSquidexLocalCache(); + app.UseSquidexCors(); + app.UseSquidexForwardingRules(); + + app.ConfigureApi(); + app.ConfigurePortal(); + app.ConfigureOrleansDashboard(); + app.ConfigureIdentityServer(); + app.ConfigureFrontend(); + + app.UsePluginsAfter(); + app.UsePlugins(); + } + } +} diff --git a/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json similarity index 100% rename from src/Squidex/appsettings.json rename to backend/src/Squidex/appsettings.json diff --git a/src/Squidex/wwwroot/client-callback-popup.html b/backend/src/Squidex/wwwroot/client-callback-popup.html similarity index 100% rename from src/Squidex/wwwroot/client-callback-popup.html rename to backend/src/Squidex/wwwroot/client-callback-popup.html diff --git a/src/Squidex/wwwroot/client-callback-silent.html b/backend/src/Squidex/wwwroot/client-callback-silent.html similarity index 100% rename from src/Squidex/wwwroot/client-callback-silent.html rename to backend/src/Squidex/wwwroot/client-callback-silent.html diff --git a/src/Squidex/wwwroot/favicon.ico b/backend/src/Squidex/wwwroot/favicon.ico similarity index 100% rename from src/Squidex/wwwroot/favicon.ico rename to backend/src/Squidex/wwwroot/favicon.ico diff --git a/src/Squidex/wwwroot/images/add-app.png b/backend/src/Squidex/wwwroot/images/add-app.png similarity index 100% rename from src/Squidex/wwwroot/images/add-app.png rename to backend/src/Squidex/wwwroot/images/add-app.png diff --git a/src/Squidex/wwwroot/images/add-blog.png b/backend/src/Squidex/wwwroot/images/add-blog.png similarity index 100% rename from src/Squidex/wwwroot/images/add-blog.png rename to backend/src/Squidex/wwwroot/images/add-blog.png diff --git a/src/Squidex/wwwroot/images/add-identity.png b/backend/src/Squidex/wwwroot/images/add-identity.png similarity index 100% rename from src/Squidex/wwwroot/images/add-identity.png rename to backend/src/Squidex/wwwroot/images/add-identity.png diff --git a/src/Squidex/wwwroot/images/add-profile.png b/backend/src/Squidex/wwwroot/images/add-profile.png similarity index 100% rename from src/Squidex/wwwroot/images/add-profile.png rename to backend/src/Squidex/wwwroot/images/add-profile.png diff --git a/src/Squidex/wwwroot/images/asset_doc.png b/backend/src/Squidex/wwwroot/images/asset_doc.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_doc.png rename to backend/src/Squidex/wwwroot/images/asset_doc.png diff --git a/src/Squidex/wwwroot/images/asset_docx.png b/backend/src/Squidex/wwwroot/images/asset_docx.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_docx.png rename to backend/src/Squidex/wwwroot/images/asset_docx.png diff --git a/src/Squidex/wwwroot/images/asset_generic.png b/backend/src/Squidex/wwwroot/images/asset_generic.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_generic.png rename to backend/src/Squidex/wwwroot/images/asset_generic.png diff --git a/src/Squidex/wwwroot/images/asset_pdf.png b/backend/src/Squidex/wwwroot/images/asset_pdf.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_pdf.png rename to backend/src/Squidex/wwwroot/images/asset_pdf.png diff --git a/src/Squidex/wwwroot/images/asset_ppt.png b/backend/src/Squidex/wwwroot/images/asset_ppt.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_ppt.png rename to backend/src/Squidex/wwwroot/images/asset_ppt.png diff --git a/src/Squidex/wwwroot/images/asset_pptx.png b/backend/src/Squidex/wwwroot/images/asset_pptx.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_pptx.png rename to backend/src/Squidex/wwwroot/images/asset_pptx.png diff --git a/src/Squidex/wwwroot/images/asset_video.png b/backend/src/Squidex/wwwroot/images/asset_video.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_video.png rename to backend/src/Squidex/wwwroot/images/asset_video.png diff --git a/src/Squidex/wwwroot/images/asset_xls.png b/backend/src/Squidex/wwwroot/images/asset_xls.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_xls.png rename to backend/src/Squidex/wwwroot/images/asset_xls.png diff --git a/src/Squidex/wwwroot/images/asset_xlsx.png b/backend/src/Squidex/wwwroot/images/asset_xlsx.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_xlsx.png rename to backend/src/Squidex/wwwroot/images/asset_xlsx.png diff --git a/src/Squidex/wwwroot/images/client.png b/backend/src/Squidex/wwwroot/images/client.png similarity index 100% rename from src/Squidex/wwwroot/images/client.png rename to backend/src/Squidex/wwwroot/images/client.png diff --git a/src/Squidex/wwwroot/images/client.svg b/backend/src/Squidex/wwwroot/images/client.svg similarity index 100% rename from src/Squidex/wwwroot/images/client.svg rename to backend/src/Squidex/wwwroot/images/client.svg diff --git a/src/Squidex/wwwroot/images/dashboard-api.png b/backend/src/Squidex/wwwroot/images/dashboard-api.png similarity index 100% rename from src/Squidex/wwwroot/images/dashboard-api.png rename to backend/src/Squidex/wwwroot/images/dashboard-api.png diff --git a/src/Squidex/wwwroot/images/dashboard-feedback.png b/backend/src/Squidex/wwwroot/images/dashboard-feedback.png similarity index 100% rename from src/Squidex/wwwroot/images/dashboard-feedback.png rename to backend/src/Squidex/wwwroot/images/dashboard-feedback.png diff --git a/src/Squidex/wwwroot/images/dashboard-github.png b/backend/src/Squidex/wwwroot/images/dashboard-github.png similarity index 100% rename from src/Squidex/wwwroot/images/dashboard-github.png rename to backend/src/Squidex/wwwroot/images/dashboard-github.png diff --git a/src/Squidex/wwwroot/images/dashboard-schema.png b/backend/src/Squidex/wwwroot/images/dashboard-schema.png similarity index 100% rename from src/Squidex/wwwroot/images/dashboard-schema.png rename to backend/src/Squidex/wwwroot/images/dashboard-schema.png diff --git a/src/Squidex/wwwroot/images/loader-white.gif b/backend/src/Squidex/wwwroot/images/loader-white.gif similarity index 100% rename from src/Squidex/wwwroot/images/loader-white.gif rename to backend/src/Squidex/wwwroot/images/loader-white.gif diff --git a/src/Squidex/wwwroot/images/loader.gif b/backend/src/Squidex/wwwroot/images/loader.gif similarity index 100% rename from src/Squidex/wwwroot/images/loader.gif rename to backend/src/Squidex/wwwroot/images/loader.gif diff --git a/src/Squidex/wwwroot/images/login-icon.png b/backend/src/Squidex/wwwroot/images/login-icon.png similarity index 100% rename from src/Squidex/wwwroot/images/login-icon.png rename to backend/src/Squidex/wwwroot/images/login-icon.png diff --git a/src/Squidex/wwwroot/images/logo-half.png b/backend/src/Squidex/wwwroot/images/logo-half.png similarity index 100% rename from src/Squidex/wwwroot/images/logo-half.png rename to backend/src/Squidex/wwwroot/images/logo-half.png diff --git a/src/Squidex/wwwroot/images/logo-small.png b/backend/src/Squidex/wwwroot/images/logo-small.png similarity index 100% rename from src/Squidex/wwwroot/images/logo-small.png rename to backend/src/Squidex/wwwroot/images/logo-small.png diff --git a/src/Squidex/wwwroot/images/logo-squared-120.png b/backend/src/Squidex/wwwroot/images/logo-squared-120.png similarity index 100% rename from src/Squidex/wwwroot/images/logo-squared-120.png rename to backend/src/Squidex/wwwroot/images/logo-squared-120.png diff --git a/src/Squidex/wwwroot/images/logo-white-small.png b/backend/src/Squidex/wwwroot/images/logo-white-small.png similarity index 100% rename from src/Squidex/wwwroot/images/logo-white-small.png rename to backend/src/Squidex/wwwroot/images/logo-white-small.png diff --git a/src/Squidex/wwwroot/images/logo-white.png b/backend/src/Squidex/wwwroot/images/logo-white.png similarity index 100% rename from src/Squidex/wwwroot/images/logo-white.png rename to backend/src/Squidex/wwwroot/images/logo-white.png diff --git a/src/Squidex/wwwroot/images/logo.png b/backend/src/Squidex/wwwroot/images/logo.png similarity index 100% rename from src/Squidex/wwwroot/images/logo.png rename to backend/src/Squidex/wwwroot/images/logo.png diff --git a/src/Squidex/wwwroot/images/onboarding-background.png b/backend/src/Squidex/wwwroot/images/onboarding-background.png similarity index 100% rename from src/Squidex/wwwroot/images/onboarding-background.png rename to backend/src/Squidex/wwwroot/images/onboarding-background.png diff --git a/src/Squidex/wwwroot/images/onboarding-step1.png b/backend/src/Squidex/wwwroot/images/onboarding-step1.png similarity index 100% rename from src/Squidex/wwwroot/images/onboarding-step1.png rename to backend/src/Squidex/wwwroot/images/onboarding-step1.png diff --git a/src/Squidex/wwwroot/images/onboarding-step2.png b/backend/src/Squidex/wwwroot/images/onboarding-step2.png similarity index 100% rename from src/Squidex/wwwroot/images/onboarding-step2.png rename to backend/src/Squidex/wwwroot/images/onboarding-step2.png diff --git a/src/Squidex/wwwroot/images/onboarding-step3.png b/backend/src/Squidex/wwwroot/images/onboarding-step3.png similarity index 100% rename from src/Squidex/wwwroot/images/onboarding-step3.png rename to backend/src/Squidex/wwwroot/images/onboarding-step3.png diff --git a/src/Squidex/wwwroot/images/onboarding-step4.png b/backend/src/Squidex/wwwroot/images/onboarding-step4.png similarity index 100% rename from src/Squidex/wwwroot/images/onboarding-step4.png rename to backend/src/Squidex/wwwroot/images/onboarding-step4.png diff --git a/src/Squidex/wwwroot/scripts/combined-editor.html b/backend/src/Squidex/wwwroot/scripts/combined-editor.html similarity index 100% rename from src/Squidex/wwwroot/scripts/combined-editor.html rename to backend/src/Squidex/wwwroot/scripts/combined-editor.html diff --git a/src/Squidex/wwwroot/scripts/context-editor.html b/backend/src/Squidex/wwwroot/scripts/context-editor.html similarity index 100% rename from src/Squidex/wwwroot/scripts/context-editor.html rename to backend/src/Squidex/wwwroot/scripts/context-editor.html diff --git a/src/Squidex/wwwroot/scripts/editor-sdk.js b/backend/src/Squidex/wwwroot/scripts/editor-sdk.js similarity index 100% rename from src/Squidex/wwwroot/scripts/editor-sdk.js rename to backend/src/Squidex/wwwroot/scripts/editor-sdk.js diff --git a/backend/src/Squidex/wwwroot/scripts/oidc-client.min.js b/backend/src/Squidex/wwwroot/scripts/oidc-client.min.js new file mode 100644 index 000000000..348bd594c --- /dev/null +++ b/backend/src/Squidex/wwwroot/scripts/oidc-client.min.js @@ -0,0 +1,47 @@ +var Oidc=function(t){var e={};function r(n){if(e[n])return e[n].exports;var i=e[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return r.m=t,r.c=e,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)r.d(n,i,function(e){return t[e]}.bind(null,i));return n},r.n=function(t){var e=t&&t.__esModule?function e(){return t.default}:function e(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=152)}([function(t,e,r){var n=r(2),i=r(19),o=r(12),s=r(13),a=r(20),u=function(t,e,r){var c,f,h,l,p=t&u.F,d=t&u.G,g=t&u.S,v=t&u.P,y=t&u.B,m=d?n:g?n[e]||(n[e]={}):(n[e]||{}).prototype,_=d?i:i[e]||(i[e]={}),S=_.prototype||(_.prototype={});for(c in d&&(r=e),r)h=((f=!p&&m&&void 0!==m[c])?m:r)[c],l=y&&f?a(h,n):v&&"function"==typeof h?a(Function.call,h):h,m&&s(m,c,h,t&u.U),_[c]!=h&&o(_,c,l),v&&S[c]!=h&&(S[c]=h)};n.core=i,u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,t.exports=u},function(t,e,r){var n=r(5);t.exports=function(t){if(!n(t))throw TypeError(t+" is not an object!");return t}},function(t,e){var r=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=r)},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=function(){function t(t,e){for(var r=0;r=4){for(var e=arguments.length,r=Array(e),n=0;n=3){for(var e=arguments.length,r=Array(e),n=0;n=2){for(var e=arguments.length,r=Array(e),n=0;n=1){for(var e=arguments.length,r=Array(e),n=0;n0?i(n(t),9007199254740991):0}},function(t,e,r){t.exports=!r(4)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(t,e,r){var n=r(1),i=r(103),o=r(24),s=Object.defineProperty;e.f=r(8)?Object.defineProperty:function t(e,r,a){if(n(e),r=o(r,!0),n(a),i)try{return s(e,r,a)}catch(t){}if("get"in a||"set"in a)throw TypeError("Accessors not supported!");return"value"in a&&(e[r]=a.value),e}},function(t,e,r){var n=r(25);t.exports=function(t){return Object(n(t))}},function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},function(t,e,r){var n=r(9),i=r(33);t.exports=r(8)?function(t,e,r){return n.f(t,e,i(1,r))}:function(t,e,r){return t[e]=r,t}},function(t,e,r){var n=r(2),i=r(12),o=r(15),s=r(34)("src"),a=r(156),u=(""+a).split("toString");r(19).inspectSource=function(t){return a.call(t)},(t.exports=function(t,e,r,a){var c="function"==typeof r;c&&(o(r,"name")||i(r,"name",e)),t[e]!==r&&(c&&(o(r,s)||i(r,s,t[e]?""+t[e]:u.join(String(e)))),t===n?t[e]=r:a?t[e]?t[e]=r:i(t,e,r):(delete t[e],i(t,e,r)))})(Function.prototype,"toString",function t(){return"function"==typeof this&&this[s]||a.call(this)})},function(t,e,r){var n=r(0),i=r(4),o=r(25),s=/"/g,a=function(t,e,r,n){var i=String(o(t)),a="<"+e;return""!==r&&(a+=" "+r+'="'+String(n).replace(s,""")+'"'),a+">"+i+""};t.exports=function(t,e){var r={};r[t]=e(a),n(n.P+n.F*i(function(){var e=""[t]('"');return e!==e.toLowerCase()||e.split('"').length>3}),"String",r)}},function(t,e){var r={}.hasOwnProperty;t.exports=function(t,e){return r.call(t,e)}},function(t,e,r){var n=r(51),i=r(25);t.exports=function(t){return n(i(t))}},function(t,e,r){var n=r(52),i=r(33),o=r(16),s=r(24),a=r(15),u=r(103),c=Object.getOwnPropertyDescriptor;e.f=r(8)?c:function t(e,r){if(e=o(e),r=s(r,!0),u)try{return c(e,r)}catch(t){}if(a(e,r))return i(!n.f.call(e,r),e[r])}},function(t,e,r){var n=r(15),i=r(10),o=r(74)("IE_PROTO"),s=Object.prototype;t.exports=Object.getPrototypeOf||function(t){return t=i(t),n(t,o)?t[o]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?s:null}},function(t,e){var r=t.exports={version:"2.6.4"};"number"==typeof __e&&(__e=r)},function(t,e,r){var n=r(11);t.exports=function(t,e,r){if(n(t),void 0===e)return t;switch(r){case 1:return function(r){return t.call(e,r)};case 2:return function(r,n){return t.call(e,r,n)};case 3:return function(r,n,i){return t.call(e,r,n,i)}}return function(){return t.apply(e,arguments)}}},function(t,e){var r={}.toString;t.exports=function(t){return r.call(t).slice(8,-1)}},function(t,e){var r=Math.ceil,n=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?n:r)(t)}},function(t,e,r){"use strict";var n=r(4);t.exports=function(t,e){return!!t&&n(function(){e?t.call(null,function(){},1):t.call(null)})}},function(t,e,r){var n=r(5);t.exports=function(t,e){if(!n(t))return t;var r,i;if(e&&"function"==typeof(r=t.toString)&&!n(i=r.call(t)))return i;if("function"==typeof(r=t.valueOf)&&!n(i=r.call(t)))return i;if(!e&&"function"==typeof(r=t.toString)&&!n(i=r.call(t)))return i;throw TypeError("Can't convert object to primitive value")}},function(t,e){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},function(t,e,r){var n=r(0),i=r(19),o=r(4);t.exports=function(t,e){var r=(i.Object||{})[t]||Object[t],s={};s[t]=e(r),n(n.S+n.F*o(function(){r(1)}),"Object",s)}},function(t,e,r){var n=r(20),i=r(51),o=r(10),s=r(7),a=r(90);t.exports=function(t,e){var r=1==t,u=2==t,c=3==t,f=4==t,h=6==t,l=5==t||h,p=e||a;return function(e,a,d){for(var g,v,y=o(e),m=i(y),_=n(a,d,3),S=s(m.length),b=0,w=r?p(e,S):u?p(e,0):void 0;S>b;b++)if((l||b in m)&&(v=_(g=m[b],b,y),t))if(r)w[b]=v;else if(v)switch(t){case 3:return!0;case 5:return g;case 6:return b;case 2:w.push(g)}else if(f)return!1;return h?-1:c||f?f:w}}},function(t,e,r){"use strict";if(r(8)){var n=r(30),i=r(2),o=r(4),s=r(0),a=r(66),u=r(98),c=r(20),f=r(40),h=r(33),l=r(12),p=r(42),d=r(22),g=r(7),v=r(131),y=r(36),m=r(24),_=r(15),S=r(46),b=r(5),w=r(10),F=r(87),E=r(37),x=r(18),A=r(38).f,k=r(89),P=r(34),C=r(6),T=r(27),R=r(56),I=r(54),O=r(92),D=r(48),N=r(61),L=r(39),M=r(91),j=r(120),U=r(9),B=r(17),H=U.f,V=B.f,K=i.RangeError,q=i.TypeError,W=i.Uint8Array,J=Array.prototype,z=u.ArrayBuffer,Y=u.DataView,G=T(0),X=T(2),$=T(3),Q=T(4),Z=T(5),tt=T(6),et=R(!0),rt=R(!1),nt=O.values,it=O.keys,ot=O.entries,st=J.lastIndexOf,at=J.reduce,ut=J.reduceRight,ct=J.join,ft=J.sort,ht=J.slice,lt=J.toString,pt=J.toLocaleString,dt=C("iterator"),gt=C("toStringTag"),vt=P("typed_constructor"),yt=P("def_constructor"),mt=a.CONSTR,_t=a.TYPED,St=a.VIEW,bt=T(1,function(t,e){return At(I(t,t[yt]),e)}),wt=o(function(){return 1===new W(new Uint16Array([1]).buffer)[0]}),Ft=!!W&&!!W.prototype.set&&o(function(){new W(1).set({})}),Et=function(t,e){var r=d(t);if(r<0||r%e)throw K("Wrong offset!");return r},xt=function(t){if(b(t)&&_t in t)return t;throw q(t+" is not a typed array!")},At=function(t,e){if(!(b(t)&&vt in t))throw q("It is not a typed array constructor!");return new t(e)},kt=function(t,e){return Pt(I(t,t[yt]),e)},Pt=function(t,e){for(var r=0,n=e.length,i=At(t,n);n>r;)i[r]=e[r++];return i},Ct=function(t,e,r){H(t,e,{get:function(){return this._d[r]}})},Tt=function t(e){var r,n,i,o,s,a,u=w(e),f=arguments.length,h=f>1?arguments[1]:void 0,l=void 0!==h,p=k(u);if(void 0!=p&&!F(p)){for(a=p.call(u),i=[],r=0;!(s=a.next()).done;r++)i.push(s.value);u=i}for(l&&f>2&&(h=c(h,arguments[2],2)),r=0,n=g(u.length),o=At(this,n);n>r;r++)o[r]=l?h(u[r],r):u[r];return o},Rt=function t(){for(var e=0,r=arguments.length,n=At(this,r);r>e;)n[e]=arguments[e++];return n},It=!!W&&o(function(){pt.call(new W(1))}),Ot=function t(){return pt.apply(It?ht.call(xt(this)):xt(this),arguments)},Dt={copyWithin:function t(e,r){return j.call(xt(this),e,r,arguments.length>2?arguments[2]:void 0)},every:function t(e){return Q(xt(this),e,arguments.length>1?arguments[1]:void 0)},fill:function t(e){return M.apply(xt(this),arguments)},filter:function t(e){return kt(this,X(xt(this),e,arguments.length>1?arguments[1]:void 0))},find:function t(e){return Z(xt(this),e,arguments.length>1?arguments[1]:void 0)},findIndex:function t(e){return tt(xt(this),e,arguments.length>1?arguments[1]:void 0)},forEach:function t(e){G(xt(this),e,arguments.length>1?arguments[1]:void 0)},indexOf:function t(e){return rt(xt(this),e,arguments.length>1?arguments[1]:void 0)},includes:function t(e){return et(xt(this),e,arguments.length>1?arguments[1]:void 0)},join:function t(e){return ct.apply(xt(this),arguments)},lastIndexOf:function t(e){return st.apply(xt(this),arguments)},map:function t(e){return bt(xt(this),e,arguments.length>1?arguments[1]:void 0)},reduce:function t(e){return at.apply(xt(this),arguments)},reduceRight:function t(e){return ut.apply(xt(this),arguments)},reverse:function t(){for(var e,r=xt(this).length,n=Math.floor(r/2),i=0;i1?arguments[1]:void 0)},sort:function t(e){return ft.call(xt(this),e)},subarray:function t(e,r){var n=xt(this),i=n.length,o=y(e,i);return new(I(n,n[yt]))(n.buffer,n.byteOffset+o*n.BYTES_PER_ELEMENT,g((void 0===r?i:y(r,i))-o))}},Nt=function t(e,r){return kt(this,ht.call(xt(this),e,r))},Lt=function t(e){xt(this);var r=Et(arguments[1],1),n=this.length,i=w(e),o=g(i.length),s=0;if(o+r>n)throw K("Wrong length!");for(;s255?255:255&n),i.v[p](r*e+i.o,n,wt)}(this,r,t)},enumerable:!0})};_?(d=r(function(t,r,n,i){f(t,d,c,"_d");var o,s,a,u,h=0,p=0;if(b(r)){if(!(r instanceof z||"ArrayBuffer"==(u=S(r))||"SharedArrayBuffer"==u))return _t in r?Pt(d,r):Tt.call(d,r);o=r,p=Et(n,e);var y=r.byteLength;if(void 0===i){if(y%e)throw K("Wrong length!");if((s=y-p)<0)throw K("Wrong length!")}else if((s=g(i)*e)+p>y)throw K("Wrong length!");a=s/e}else a=v(r),o=new z(s=a*e);for(l(t,"_d",{b:o,o:p,l:s,e:a,v:new Y(o)});hdocument.F=Object<\/script>"),t.close(),u=t.F;n--;)delete u.prototype[o[n]];return u()};t.exports=Object.create||function t(e,r){var o;return null!==e?(a.prototype=n(e),o=new a,a.prototype=null,o[s]=e):o=u(),void 0===r?o:i(o,r)}},function(t,e,r){var n=r(105),i=r(75).concat("length","prototype");e.f=Object.getOwnPropertyNames||function t(e){return n(e,i)}},function(t,e,r){"use strict";var n=r(2),i=r(9),o=r(8),s=r(6)("species");t.exports=function(t){var e=n[t];o&&e&&!e[s]&&i.f(e,s,{configurable:!0,get:function(){return this}})}},function(t,e){t.exports=function(t,e,r,n){if(!(t instanceof e)||void 0!==n&&n in t)throw TypeError(r+": incorrect invocation!");return t}},function(t,e,r){var n=r(20),i=r(118),o=r(87),s=r(1),a=r(7),u=r(89),c={},f={};(e=t.exports=function(t,e,r,h,l){var p,d,g,v,y=l?function(){return t}:u(t),m=n(r,h,e?2:1),_=0;if("function"!=typeof y)throw TypeError(t+" is not iterable!");if(o(y)){for(p=a(t.length);p>_;_++)if((v=e?m(s(d=t[_])[0],d[1]):m(t[_]))===c||v===f)return v}else for(g=y.call(t);!(d=g.next()).done;)if((v=i(g,m,d.value,e))===c||v===f)return v}).BREAK=c,e.RETURN=f},function(t,e,r){var n=r(13);t.exports=function(t,e,r){for(var i in e)n(t,i,e[i],r);return t}},function(t,e,r){var n=r(5);t.exports=function(t,e){if(!n(t)||t._t!==e)throw TypeError("Incompatible receiver, "+e+" required!");return t}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=function(){function t(t,e){for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:o.JsonService;if(function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!e)throw i.Log.error("MetadataService: No settings passed to MetadataService"),new Error("settings");this._settings=e,this._jsonService=new r(["application/jwk-set+json"])}return t.prototype.getMetadata=function t(){var e=this;return this._settings.metadata?(i.Log.debug("MetadataService.getMetadata: Returning metadata from settings"),Promise.resolve(this._settings.metadata)):this.metadataUrl?(i.Log.debug("MetadataService.getMetadata: getting metadata from",this.metadataUrl),this._jsonService.getJson(this.metadataUrl).then(function(t){return i.Log.debug("MetadataService.getMetadata: json received"),e._settings.metadata=t,t})):(i.Log.error("MetadataService.getMetadata: No authority or metadataUrl configured on settings"),Promise.reject(new Error("No authority or metadataUrl configured on settings")))},t.prototype.getIssuer=function t(){return this._getMetadataProperty("issuer")},t.prototype.getAuthorizationEndpoint=function t(){return this._getMetadataProperty("authorization_endpoint")},t.prototype.getUserInfoEndpoint=function t(){return this._getMetadataProperty("userinfo_endpoint")},t.prototype.getTokenEndpoint=function t(){var e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];return this._getMetadataProperty("token_endpoint",e)},t.prototype.getCheckSessionIframe=function t(){return this._getMetadataProperty("check_session_iframe",!0)},t.prototype.getEndSessionEndpoint=function t(){return this._getMetadataProperty("end_session_endpoint",!0)},t.prototype.getRevocationEndpoint=function t(){return this._getMetadataProperty("revocation_endpoint",!0)},t.prototype.getKeysEndpoint=function t(){return this._getMetadataProperty("jwks_uri",!0)},t.prototype._getMetadataProperty=function t(e){var r=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return i.Log.debug("MetadataService.getMetadataProperty for: "+e),this.getMetadata().then(function(t){if(i.Log.debug("MetadataService.getMetadataProperty: metadata recieved"),void 0===t[e]){if(!0===r)return void i.Log.warn("MetadataService.getMetadataProperty: Metadata does not contain optional property "+e);throw i.Log.error("MetadataService.getMetadataProperty: Metadata does not contain property "+e),new Error("Metadata does not contain property "+e)}return t[e]})},t.prototype.getSigningKeys=function t(){var e=this;return this._settings.signingKeys?(i.Log.debug("MetadataService.getSigningKeys: Returning signingKeys from settings"),Promise.resolve(this._settings.signingKeys)):this._getMetadataProperty("jwks_uri").then(function(t){return i.Log.debug("MetadataService.getSigningKeys: jwks_uri received",t),e._jsonService.getJson(t).then(function(t){if(i.Log.debug("MetadataService.getSigningKeys: key set received",t),!t.keys)throw i.Log.error("MetadataService.getSigningKeys: Missing keys on keyset"),new Error("Missing keys on keyset");return e._settings.signingKeys=t.keys,e._settings.signingKeys})})},n(t,[{key:"metadataUrl",get:function t(){return this._metadataUrl||(this._settings.metadataUrl?this._metadataUrl=this._settings.metadataUrl:(this._metadataUrl=this._settings.authority,this._metadataUrl&&this._metadataUrl.indexOf(".well-known/openid-configuration")<0&&("/"!==this._metadataUrl[this._metadataUrl.length-1]&&(this._metadataUrl+="/"),this._metadataUrl+=".well-known/openid-configuration"))),this._metadataUrl}}]),t}()},function(t,e,r){var n=r(19),i=r(2),o=i["__core-js_shared__"]||(i["__core-js_shared__"]={});(t.exports=function(t,e){return o[t]||(o[t]=void 0!==e?e:{})})("versions",[]).push({version:n.version,mode:r(30)?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},function(t,e,r){var n=r(21);t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==n(t)?t.split(""):Object(t)}},function(t,e){e.f={}.propertyIsEnumerable},function(t,e,r){"use strict";var n=r(1);t.exports=function(){var t=n(this),e="";return t.global&&(e+="g"),t.ignoreCase&&(e+="i"),t.multiline&&(e+="m"),t.unicode&&(e+="u"),t.sticky&&(e+="y"),e}},function(t,e,r){var n=r(1),i=r(11),o=r(6)("species");t.exports=function(t,e){var r,s=n(t).constructor;return void 0===s||void 0==(r=n(s)[o])?e:i(r)}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.UrlUtility=void 0;var n=r(3),i=r(44);e.UrlUtility=function(){function t(){!function e(t,r){if(!(t instanceof r))throw new TypeError("Cannot call a class as a function")}(this,t)}return t.addQueryParam=function t(e,r,n){return e.indexOf("?")<0&&(e+="?"),"?"!==e[e.length-1]&&(e+="&"),e+=encodeURIComponent(r),e+="=",e+=encodeURIComponent(n)},t.parseUrlFragment=function t(e){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"#",o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:i.Global;"string"!=typeof e&&(e=o.location.href);var s=e.lastIndexOf(r);s>=0&&(e=e.substr(s+1)),"?"===r&&(s=e.indexOf("#"))>=0&&(e=e.substr(0,s));for(var a,u={},c=/([^&=]+)=([^&]*)/g,f=0;a=c.exec(e);)if(u[decodeURIComponent(a[1])]=decodeURIComponent(a[2]),f++>50)return n.Log.error("UrlUtility.parseUrlFragment: response exceeded expected number of parameters",e),{error:"Response exceeded expected number of parameters"};for(var h in u)return u;return{}},t}()},function(t,e,r){var n=r(16),i=r(7),o=r(36);t.exports=function(t){return function(e,r,s){var a,u=n(e),c=i(u.length),f=o(s,c);if(t&&r!=r){for(;c>f;)if((a=u[f++])!=a)return!0}else for(;c>f;f++)if((t||f in u)&&u[f]===r)return t||f||0;return!t&&-1}}},function(t,e){e.f=Object.getOwnPropertySymbols},function(t,e,r){var n=r(21);t.exports=Array.isArray||function t(e){return"Array"==n(e)}},function(t,e,r){var n=r(22),i=r(25);t.exports=function(t){return function(e,r){var o,s,a=String(i(e)),u=n(r),c=a.length;return u<0||u>=c?t?"":void 0:(o=a.charCodeAt(u))<55296||o>56319||u+1===c||(s=a.charCodeAt(u+1))<56320||s>57343?t?a.charAt(u):o:t?a.slice(u,u+2):s-56320+(o-55296<<10)+65536}}},function(t,e,r){var n=r(5),i=r(21),o=r(6)("match");t.exports=function(t){var e;return n(t)&&(void 0!==(e=t[o])?!!e:"RegExp"==i(t))}},function(t,e,r){var n=r(6)("iterator"),i=!1;try{var o=[7][n]();o.return=function(){i=!0},Array.from(o,function(){throw 2})}catch(t){}t.exports=function(t,e){if(!e&&!i)return!1;var r=!1;try{var o=[7],s=o[n]();s.next=function(){return{done:r=!0}},o[n]=function(){return s},t(o)}catch(t){}return r}},function(t,e,r){"use strict";var n=r(46),i=RegExp.prototype.exec;t.exports=function(t,e){var r=t.exec;if("function"==typeof r){var o=r.call(t,e);if("object"!=typeof o)throw new TypeError("RegExp exec method returned something other than an Object or null");return o}if("RegExp"!==n(t))throw new TypeError("RegExp#exec called on incompatible receiver");return i.call(t,e)}},function(t,e,r){"use strict";r(122);var n=r(13),i=r(12),o=r(4),s=r(25),a=r(6),u=r(93),c=a("species"),f=!o(function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$")}),h=function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var r="ab".split(t);return 2===r.length&&"a"===r[0]&&"b"===r[1]}();t.exports=function(t,e,r){var l=a(t),p=!o(function(){var e={};return e[l]=function(){return 7},7!=""[t](e)}),d=p?!o(function(){var e=!1,r=/a/;return r.exec=function(){return e=!0,null},"split"===t&&(r.constructor={},r.constructor[c]=function(){return r}),r[l](""),!e}):void 0;if(!p||!d||"replace"===t&&!f||"split"===t&&!h){var g=/./[l],v=r(s,l,""[t],function t(e,r,n,i,o){return r.exec===u?p&&!o?{done:!0,value:g.call(r,n,i)}:{done:!0,value:e.call(n,r,i)}:{done:!1}}),y=v[0],m=v[1];n(String.prototype,t,y),i(RegExp.prototype,l,2==e?function(t,e){return m.call(t,this,e)}:function(t){return m.call(t,this)})}}},function(t,e,r){var n=r(2).navigator;t.exports=n&&n.userAgent||""},function(t,e,r){"use strict";var n=r(2),i=r(0),o=r(13),s=r(42),a=r(31),u=r(41),c=r(40),f=r(5),h=r(4),l=r(61),p=r(45),d=r(79);t.exports=function(t,e,r,g,v,y){var m=n[t],_=m,S=v?"set":"add",b=_&&_.prototype,w={},F=function(t){var e=b[t];o(b,t,"delete"==t?function(t){return!(y&&!f(t))&&e.call(this,0===t?0:t)}:"has"==t?function t(r){return!(y&&!f(r))&&e.call(this,0===r?0:r)}:"get"==t?function t(r){return y&&!f(r)?void 0:e.call(this,0===r?0:r)}:"add"==t?function t(r){return e.call(this,0===r?0:r),this}:function t(r,n){return e.call(this,0===r?0:r,n),this})};if("function"==typeof _&&(y||b.forEach&&!h(function(){(new _).entries().next()}))){var E=new _,x=E[S](y?{}:-0,1)!=E,A=h(function(){E.has(1)}),k=l(function(t){new _(t)}),P=!y&&h(function(){for(var t=new _,e=5;e--;)t[S](e,e);return!t.has(-0)});k||((_=e(function(e,r){c(e,_,t);var n=d(new m,e,_);return void 0!=r&&u(r,v,n[S],n),n})).prototype=b,b.constructor=_),(A||P)&&(F("delete"),F("has"),v&&F("get")),(P||x)&&F(S),y&&b.clear&&delete b.clear}else _=g.getConstructor(e,t,v,S),s(_.prototype,r),a.NEED=!0;return p(_,t),w[t]=_,i(i.G+i.W+i.F*(_!=m),w),y||g.setStrong(_,t,v),_}},function(t,e,r){for(var n,i=r(2),o=r(12),s=r(34),a=s("typed_array"),u=s("view"),c=!(!i.ArrayBuffer||!i.DataView),f=c,h=0,l="Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array".split(",");h<9;)(n=i[l[h++]])?(o(n.prototype,a,!0),o(n.prototype,u,!0)):f=!1;t.exports={ABV:c,CONSTR:f,TYPED:a,VIEW:u}},function(t,e,r){"use strict";t.exports=r(30)||!r(4)(function(){var t=Math.random();__defineSetter__.call(null,t,function(){}),delete r(2)[t]})},function(t,e,r){"use strict";var n=r(0);t.exports=function(t){n(n.S,t,{of:function t(){for(var e=arguments.length,r=new Array(e);e--;)r[e]=arguments[e];return new this(r)}})}},function(t,e,r){"use strict";var n=r(0),i=r(11),o=r(20),s=r(41);t.exports=function(t){n(n.S,t,{from:function t(e){var r,n,a,u,c=arguments[1];return i(this),(r=void 0!==c)&&i(c),void 0==e?new this:(n=[],r?(a=0,u=o(c,arguments[2],2),s(e,!1,function(t){n.push(u(t,a++))})):s(e,!1,n.push,n),new this(n))}})}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.JoseUtil=void 0;var n=r(358),i=function o(t){return t&&t.__esModule?t:{default:t}}(r(364));e.JoseUtil=(0,i.default)({jws:n.jws,KeyUtil:n.KeyUtil,X509:n.X509,crypto:n.crypto,hextob64u:n.hextob64u,b64tohex:n.b64tohex,AllowedSigningAlgs:n.AllowedSigningAlgs})},function(t,e){var r;r=function(){return this}();try{r=r||new Function("return this")()}catch(t){"object"==typeof window&&(r=window)}t.exports=r},function(t,e,r){var n=r(5),i=r(2).document,o=n(i)&&n(i.createElement);t.exports=function(t){return o?i.createElement(t):{}}},function(t,e,r){var n=r(2),i=r(19),o=r(30),s=r(104),a=r(9).f;t.exports=function(t){var e=i.Symbol||(i.Symbol=o?{}:n.Symbol||{});"_"==t.charAt(0)||t in e||a(e,t,{value:s.f(t)})}},function(t,e,r){var n=r(50)("keys"),i=r(34);t.exports=function(t){return n[t]||(n[t]=i(t))}},function(t,e){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(t,e,r){var n=r(2).document;t.exports=n&&n.documentElement},function(t,e,r){var n=r(5),i=r(1),o=function(t,e){if(i(t),!n(e)&&null!==e)throw TypeError(e+": can't set as prototype!")};t.exports={set:Object.setPrototypeOf||("__proto__"in{}?function(t,e,n){try{(n=r(20)(Function.call,r(17).f(Object.prototype,"__proto__").set,2))(t,[]),e=!(t instanceof Array)}catch(t){e=!0}return function t(r,i){return o(r,i),e?r.__proto__=i:n(r,i),r}}({},!1):void 0),check:o}},function(t,e){t.exports="\t\n\v\f\r   ᠎              \u2028\u2029\ufeff"},function(t,e,r){var n=r(5),i=r(77).set;t.exports=function(t,e,r){var o,s=e.constructor;return s!==r&&"function"==typeof s&&(o=s.prototype)!==r.prototype&&n(o)&&i&&i(t,o),t}},function(t,e,r){"use strict";var n=r(22),i=r(25);t.exports=function t(e){var r=String(i(this)),o="",s=n(e);if(s<0||s==1/0)throw RangeError("Count can't be negative");for(;s>0;(s>>>=1)&&(r+=r))1&s&&(o+=r);return o}},function(t,e){t.exports=Math.sign||function t(e){return 0==(e=+e)||e!=e?e:e<0?-1:1}},function(t,e){var r=Math.expm1;t.exports=!r||r(10)>22025.465794806718||r(10)<22025.465794806718||-2e-17!=r(-2e-17)?function t(e){return 0==(e=+e)?e:e>-1e-6&&e<1e-6?e+e*e/2:Math.exp(e)-1}:r},function(t,e,r){"use strict";var n=r(30),i=r(0),o=r(13),s=r(12),a=r(48),u=r(84),c=r(45),f=r(18),h=r(6)("iterator"),l=!([].keys&&"next"in[].keys()),p=function(){return this};t.exports=function(t,e,r,d,g,v,y){u(r,e,d);var m,_,S,b=function(t){if(!l&&t in x)return x[t];switch(t){case"keys":return function e(){return new r(this,t)};case"values":return function e(){return new r(this,t)}}return function e(){return new r(this,t)}},w=e+" Iterator",F="values"==g,E=!1,x=t.prototype,A=x[h]||x["@@iterator"]||g&&x[g],k=A||b(g),P=g?F?b("entries"):k:void 0,C="Array"==e&&x.entries||A;if(C&&(S=f(C.call(new t)))!==Object.prototype&&S.next&&(c(S,w,!0),n||"function"==typeof S[h]||s(S,h,p)),F&&A&&"values"!==A.name&&(E=!0,k=function t(){return A.call(this)}),n&&!y||!l&&!E&&x[h]||s(x,h,k),a[e]=k,a[w]=p,g)if(m={values:F?k:b("values"),keys:v?k:b("keys"),entries:P},y)for(_ in m)_ in x||o(x,_,m[_]);else i(i.P+i.F*(l||E),e,m);return m}},function(t,e,r){"use strict";var n=r(37),i=r(33),o=r(45),s={};r(12)(s,r(6)("iterator"),function(){return this}),t.exports=function(t,e,r){t.prototype=n(s,{next:i(1,r)}),o(t,e+" Iterator")}},function(t,e,r){var n=r(60),i=r(25);t.exports=function(t,e,r){if(n(e))throw TypeError("String#"+r+" doesn't accept regex!");return String(i(t))}},function(t,e,r){var n=r(6)("match");t.exports=function(t){var e=/./;try{"/./"[t](e)}catch(r){try{return e[n]=!1,!"/./"[t](e)}catch(t){}}return!0}},function(t,e,r){var n=r(48),i=r(6)("iterator"),o=Array.prototype;t.exports=function(t){return void 0!==t&&(n.Array===t||o[i]===t)}},function(t,e,r){"use strict";var n=r(9),i=r(33);t.exports=function(t,e,r){e in t?n.f(t,e,i(0,r)):t[e]=r}},function(t,e,r){var n=r(46),i=r(6)("iterator"),o=r(48);t.exports=r(19).getIteratorMethod=function(t){if(void 0!=t)return t[i]||t["@@iterator"]||o[n(t)]}},function(t,e,r){var n=r(245);t.exports=function(t,e){return new(n(t))(e)}},function(t,e,r){"use strict";var n=r(10),i=r(36),o=r(7);t.exports=function t(e){for(var r=n(this),s=o(r.length),a=arguments.length,u=i(a>1?arguments[1]:void 0,s),c=a>2?arguments[2]:void 0,f=void 0===c?s:i(c,s);f>u;)r[u++]=e;return r}},function(t,e,r){"use strict";var n=r(32),i=r(121),o=r(48),s=r(16);t.exports=r(83)(Array,"Array",function(t,e){this._t=s(t),this._i=0,this._k=e},function(){var t=this._t,e=this._k,r=this._i++;return!t||r>=t.length?(this._t=void 0,i(1)):i(0,"keys"==e?r:"values"==e?t[r]:[r,t[r]])},"values"),o.Arguments=o.Array,n("keys"),n("values"),n("entries")},function(t,e,r){"use strict";var n,i,o=r(53),s=RegExp.prototype.exec,a=String.prototype.replace,u=s,c=(n=/a/,i=/b*/g,s.call(n,"a"),s.call(i,"a"),0!==n.lastIndex||0!==i.lastIndex),f=void 0!==/()??/.exec("")[1];(c||f)&&(u=function t(e){var r,n,i,u,h=this;return f&&(n=new RegExp("^"+h.source+"$(?!\\s)",o.call(h))),c&&(r=h.lastIndex),i=s.call(h,e),c&&i&&(h.lastIndex=h.global?i.index+i[0].length:r),f&&i&&i.length>1&&a.call(i[0],n,function(){for(u=1;ui;)r.push(arguments[i++]);return y[++v]=function(){a("function"==typeof e?e:Function(e),r)},n(v),v},p=function t(e){delete y[e]},"process"==r(21)(h)?n=function(t){h.nextTick(s(m,t,1))}:g&&g.now?n=function(t){g.now(s(m,t,1))}:d?(o=(i=new d).port2,i.port1.onmessage=_,n=s(o.postMessage,o,1)):f.addEventListener&&"function"==typeof postMessage&&!f.importScripts?(n=function(t){f.postMessage(t+"","*")},f.addEventListener("message",_,!1)):n="onreadystatechange"in c("script")?function(t){u.appendChild(c("script")).onreadystatechange=function(){u.removeChild(this),m.call(t)}}:function(t){setTimeout(s(m,t,1),0)}),t.exports={set:l,clear:p}},function(t,e,r){var n=r(2),i=r(95).set,o=n.MutationObserver||n.WebKitMutationObserver,s=n.process,a=n.Promise,u="process"==r(21)(s);t.exports=function(){var t,e,r,c=function(){var n,i;for(u&&(n=s.domain)&&n.exit();t;){i=t.fn,t=t.next;try{i()}catch(n){throw t?r():e=void 0,n}}e=void 0,n&&n.enter()};if(u)r=function(){s.nextTick(c)};else if(!o||n.navigator&&n.navigator.standalone)if(a&&a.resolve){var f=a.resolve(void 0);r=function(){f.then(c)}}else r=function(){i.call(n,c)};else{var h=!0,l=document.createTextNode("");new o(c).observe(l,{characterData:!0}),r=function(){l.data=h=!h}}return function(n){var i={fn:n,next:void 0};e&&(e.next=i),t||(t=i,r()),e=i}}},function(t,e,r){"use strict";var n=r(11);t.exports.f=function(t){return new function e(t){var e,r;this.promise=new t(function(t,n){if(void 0!==e||void 0!==r)throw TypeError("Bad Promise constructor");e=t,r=n}),this.resolve=n(e),this.reject=n(r)}(t)}},function(t,e,r){"use strict";var n=r(2),i=r(8),o=r(30),s=r(66),a=r(12),u=r(42),c=r(4),f=r(40),h=r(22),l=r(7),p=r(131),d=r(38).f,g=r(9).f,v=r(91),y=r(45),m="prototype",_="Wrong index!",S=n.ArrayBuffer,b=n.DataView,w=n.Math,F=n.RangeError,E=n.Infinity,x=S,A=w.abs,k=w.pow,P=w.floor,C=w.log,T=w.LN2,R=i?"_b":"buffer",I=i?"_l":"byteLength",O=i?"_o":"byteOffset";function D(t,e,r){var n,i,o,s=new Array(r),a=8*r-e-1,u=(1<>1,f=23===e?k(2,-24)-k(2,-77):0,h=0,l=t<0||0===t&&1/t<0?1:0;for((t=A(t))!=t||t===E?(i=t!=t?1:0,n=u):(n=P(C(t)/T),t*(o=k(2,-n))<1&&(n--,o*=2),(t+=n+c>=1?f/o:f*k(2,1-c))*o>=2&&(n++,o/=2),n+c>=u?(i=0,n=u):n+c>=1?(i=(t*o-1)*k(2,e),n+=c):(i=t*k(2,c-1)*k(2,e),n=0));e>=8;s[h++]=255&i,i/=256,e-=8);for(n=n<0;s[h++]=255&n,n/=256,a-=8);return s[--h]|=128*l,s}function N(t,e,r){var n,i=8*r-e-1,o=(1<>1,a=i-7,u=r-1,c=t[u--],f=127&c;for(c>>=7;a>0;f=256*f+t[u],u--,a-=8);for(n=f&(1<<-a)-1,f>>=-a,a+=e;a>0;n=256*n+t[u],u--,a-=8);if(0===f)f=1-s;else{if(f===o)return n?NaN:c?-E:E;n+=k(2,e),f-=s}return(c?-1:1)*n*k(2,f-e)}function L(t){return t[3]<<24|t[2]<<16|t[1]<<8|t[0]}function M(t){return[255&t]}function j(t){return[255&t,t>>8&255]}function U(t){return[255&t,t>>8&255,t>>16&255,t>>24&255]}function B(t){return D(t,52,8)}function H(t){return D(t,23,4)}function V(t,e,r){g(t[m],e,{get:function(){return this[r]}})}function K(t,e,r,n){var i=p(+r);if(i+e>t[I])throw F(_);var o=t[R]._b,s=i+t[O],a=o.slice(s,s+e);return n?a:a.reverse()}function q(t,e,r,n,i,o){var s=p(+r);if(s+e>t[I])throw F(_);for(var a=t[R]._b,u=s+t[O],c=n(+i),f=0;fY;)(W=z[Y++])in S||a(S,W,x[W]);o||(J.constructor=S)}var G=new b(new S(2)),X=b[m].setInt8;G.setInt8(0,2147483648),G.setInt8(1,2147483649),!G.getInt8(0)&&G.getInt8(1)||u(b[m],{setInt8:function t(e,r){X.call(this,e,r<<24>>24)},setUint8:function t(e,r){X.call(this,e,r<<24>>24)}},!0)}else S=function t(e){f(this,S,"ArrayBuffer");var r=p(e);this._b=v.call(new Array(r),0),this[I]=r},b=function t(e,r,n){f(this,b,"DataView"),f(e,S,"DataView");var i=e[I],o=h(r);if(o<0||o>i)throw F("Wrong offset!");if(o+(n=void 0===n?i-o:l(n))>i)throw F("Wrong length!");this[R]=e,this[O]=o,this[I]=n},i&&(V(S,"byteLength","_l"),V(b,"buffer","_b"),V(b,"byteLength","_l"),V(b,"byteOffset","_o")),u(b[m],{getInt8:function t(e){return K(this,1,e)[0]<<24>>24},getUint8:function t(e){return K(this,1,e)[0]},getInt16:function t(e){var r=K(this,2,e,arguments[1]);return(r[1]<<8|r[0])<<16>>16},getUint16:function t(e){var r=K(this,2,e,arguments[1]);return r[1]<<8|r[0]},getInt32:function t(e){return L(K(this,4,e,arguments[1]))},getUint32:function t(e){return L(K(this,4,e,arguments[1]))>>>0},getFloat32:function t(e){return N(K(this,4,e,arguments[1]),23,4)},getFloat64:function t(e){return N(K(this,8,e,arguments[1]),52,8)},setInt8:function t(e,r){q(this,1,e,M,r)},setUint8:function t(e,r){q(this,1,e,M,r)},setInt16:function t(e,r){q(this,2,e,j,r,arguments[2])},setUint16:function t(e,r){q(this,2,e,j,r,arguments[2])},setInt32:function t(e,r){q(this,4,e,U,r,arguments[2])},setUint32:function t(e,r){q(this,4,e,U,r,arguments[2])},setFloat32:function t(e,r){q(this,4,e,H,r,arguments[2])},setFloat64:function t(e,r){q(this,8,e,B,r,arguments[2])}});y(S,"ArrayBuffer"),y(b,"DataView"),a(b[m],s.VIEW,!0),e.ArrayBuffer=S,e.DataView=b},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.OidcClientSettings=void 0;var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function t(t,e){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{},r=e.authority,i=e.metadataUrl,o=e.metadata,p=e.signingKeys,d=e.client_id,g=e.client_secret,v=e.response_type,y=void 0===v?c:v,m=e.scope,_=void 0===m?f:m,S=e.redirect_uri,b=e.post_logout_redirect_uri,w=e.prompt,F=e.display,E=e.max_age,x=e.ui_locales,A=e.acr_values,k=e.resource,P=e.response_mode,C=e.filterProtocolClaims,T=void 0===C||C,R=e.loadUserInfo,I=void 0===R||R,O=e.staleStateAge,D=void 0===O?h:O,N=e.clockSkew,L=void 0===N?l:N,M=e.userInfoJwtIssuer,j=void 0===M?"OP":M,U=e.stateStore,B=void 0===U?new s.WebStorageStateStore:U,H=e.ResponseValidatorCtor,V=void 0===H?a.ResponseValidator:H,K=e.MetadataServiceCtor,q=void 0===K?u.MetadataService:K,W=e.extraQueryParams,J=void 0===W?{}:W;!function z(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this._authority=r,this._metadataUrl=i,this._metadata=o,this._signingKeys=p,this._client_id=d,this._client_secret=g,this._response_type=y,this._scope=_,this._redirect_uri=S,this._post_logout_redirect_uri=b,this._prompt=w,this._display=F,this._max_age=E,this._ui_locales=x,this._acr_values=A,this._resource=k,this._response_mode=P,this._filterProtocolClaims=!!T,this._loadUserInfo=!!I,this._staleStateAge=D,this._clockSkew=L,this._userInfoJwtIssuer=j,this._stateStore=B,this._validator=new V(this),this._metadataService=new q(this),this._extraQueryParams="object"===(void 0===J?"undefined":n(J))?J:{}}return i(t,[{key:"client_id",get:function t(){return this._client_id},set:function t(e){if(this._client_id)throw o.Log.error("OidcClientSettings.set_client_id: client_id has already been assigned."),new Error("client_id has already been assigned.");this._client_id=e}},{key:"client_secret",get:function t(){return this._client_secret}},{key:"response_type",get:function t(){return this._response_type}},{key:"scope",get:function t(){return this._scope}},{key:"redirect_uri",get:function t(){return this._redirect_uri}},{key:"post_logout_redirect_uri",get:function t(){return this._post_logout_redirect_uri}},{key:"prompt",get:function t(){return this._prompt}},{key:"display",get:function t(){return this._display}},{key:"max_age",get:function t(){return this._max_age}},{key:"ui_locales",get:function t(){return this._ui_locales}},{key:"acr_values",get:function t(){return this._acr_values}},{key:"resource",get:function t(){return this._resource}},{key:"response_mode",get:function t(){return this._response_mode}},{key:"authority",get:function t(){return this._authority},set:function t(e){if(this._authority)throw o.Log.error("OidcClientSettings.set_authority: authority has already been assigned."),new Error("authority has already been assigned.");this._authority=e}},{key:"metadataUrl",get:function t(){return this._metadataUrl||(this._metadataUrl=this.authority,this._metadataUrl&&this._metadataUrl.indexOf(".well-known/openid-configuration")<0&&("/"!==this._metadataUrl[this._metadataUrl.length-1]&&(this._metadataUrl+="/"),this._metadataUrl+=".well-known/openid-configuration")),this._metadataUrl}},{key:"metadata",get:function t(){return this._metadata},set:function t(e){this._metadata=e}},{key:"signingKeys",get:function t(){return this._signingKeys},set:function t(e){this._signingKeys=e}},{key:"filterProtocolClaims",get:function t(){return this._filterProtocolClaims}},{key:"loadUserInfo",get:function t(){return this._loadUserInfo}},{key:"staleStateAge",get:function t(){return this._staleStateAge}},{key:"clockSkew",get:function t(){return this._clockSkew}},{key:"userInfoJwtIssuer",get:function t(){return this._userInfoJwtIssuer}},{key:"stateStore",get:function t(){return this._stateStore}},{key:"validator",get:function t(){return this._validator}},{key:"metadataService",get:function t(){return this._metadataService}},{key:"extraQueryParams",get:function t(){return this._extraQueryParams},set:function t(e){"object"===(void 0===e?"undefined":n(e))?this._extraQueryParams=e:this._extraQueryParams={}}}]),t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.WebStorageStateStore=void 0;var n=r(3),i=r(44);e.WebStorageStateStore=function(){function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=e.prefix,n=void 0===r?"oidc.":r,o=e.store,s=void 0===o?i.Global.localStorage:o;!function a(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this._store=s,this._prefix=n}return t.prototype.set=function t(e,r){return n.Log.debug("WebStorageStateStore.set",e),e=this._prefix+e,this._store.setItem(e,r),Promise.resolve()},t.prototype.get=function t(e){n.Log.debug("WebStorageStateStore.get",e),e=this._prefix+e;var r=this._store.getItem(e);return Promise.resolve(r)},t.prototype.remove=function t(e){n.Log.debug("WebStorageStateStore.remove",e),e=this._prefix+e;var r=this._store.getItem(e);return this._store.removeItem(e),Promise.resolve(r)},t.prototype.getAllKeys=function t(){n.Log.debug("WebStorageStateStore.getAllKeys");for(var e=[],r=0;r0&&void 0!==arguments[0]?arguments[0]:null,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:i.Global.XMLHttpRequest,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;!function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),e&&Array.isArray(e)?this._contentTypes=e.slice():this._contentTypes=[],this._contentTypes.push("application/json"),n&&this._contentTypes.push("application/jwt"),this._XMLHttpRequest=r,this._jwtHandler=n}return t.prototype.getJson=function t(e,r){var i=this;if(!e)throw n.Log.error("JsonService.getJson: No url passed"),new Error("url");return n.Log.debug("JsonService.getJson, url: ",e),new Promise(function(t,o){var s=new i._XMLHttpRequest;s.open("GET",e);var a=i._contentTypes,u=i._jwtHandler;s.onload=function(){if(n.Log.debug("JsonService.getJson: HTTP response received, status",s.status),200===s.status){var r=s.getResponseHeader("Content-Type");if(r){var i=a.find(function(t){if(r.startsWith(t))return!0});if("application/jwt"==i)return void u(s).then(t,o);if(i)try{return void t(JSON.parse(s.responseText))}catch(t){return n.Log.error("JsonService.getJson: Error parsing JSON response",t.message),void o(t)}}o(Error("Invalid response Content-Type: "+r+", from URL: "+e))}else o(Error(s.statusText+" ("+s.status+")"))},s.onerror=function(){n.Log.error("JsonService.getJson: network error"),o(Error("Network Error"))},r&&(n.Log.debug("JsonService.getJson: token passed, setting Authorization header"),s.setRequestHeader("Authorization","Bearer "+r)),s.send()})},t.prototype.postForm=function t(e,r){var i=this;if(!e)throw n.Log.error("JsonService.postForm: No url passed"),new Error("url");return n.Log.debug("JsonService.postForm, url: ",e),new Promise(function(t,o){var s=new i._XMLHttpRequest;s.open("POST",e);var a=i._contentTypes;s.onload=function(){if(n.Log.debug("JsonService.postForm: HTTP response received, status",s.status),200!==s.status){if(400===s.status)if(i=s.getResponseHeader("Content-Type"))if(a.find(function(t){if(i.startsWith(t))return!0}))try{var r=JSON.parse(s.responseText);if(r&&r.error)return n.Log.error("JsonService.postForm: Error from server: ",r.error),void o(new Error(r.error))}catch(t){return n.Log.error("JsonService.postForm: Error parsing JSON response",t.message),void o(t)}o(Error(s.statusText+" ("+s.status+")"))}else{var i;if((i=s.getResponseHeader("Content-Type"))&&a.find(function(t){if(i.startsWith(t))return!0}))try{return void t(JSON.parse(s.responseText))}catch(t){return n.Log.error("JsonService.postForm: Error parsing JSON response",t.message),void o(t)}o(Error("Invalid response Content-Type: "+i+", from URL: "+e))}},s.onerror=function(){n.Log.error("JsonService.postForm: network error"),o(Error("Network Error"))};var u="";for(var c in r){var f=r[c];f&&(u.length>0&&(u+="&"),u+=encodeURIComponent(c),u+="=",u+=encodeURIComponent(f))}s.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),s.send(u)})},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.State=void 0;var n=function(){function t(t,e){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{},r=e.id,n=e.data,i=e.created,s=e.request_type;!function a(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this._id=r||(0,o.default)(),this._data=n,this._created="number"==typeof i&&i>0?i:parseInt(Date.now()/1e3),this._request_type=s}return t.prototype.toStorageString=function t(){return i.Log.debug("State.toStorageString"),JSON.stringify({id:this.id,data:this.data,created:this.created,request_type:this.request_type})},t.fromStorageString=function e(r){return i.Log.debug("State.fromStorageString"),new t(JSON.parse(r))},t.clearStaleState=function e(r,n){var o=Date.now()/1e3-n;return r.getAllKeys().then(function(e){i.Log.debug("State.clearStaleState: got keys",e);for(var n=[],s=function s(a){var c=e[a];u=r.get(c).then(function(e){var n=!1;if(e)try{var s=t.fromStorageString(e);i.Log.debug("State.clearStaleState: got item from key: ",c,s.created),s.created<=o&&(n=!0)}catch(t){i.Log.error("State.clearStaleState: Error parsing state for key",c,t.message),n=!0}else i.Log.debug("State.clearStaleState: no item in storage for key: ",c),n=!0;if(n)return i.Log.debug("State.clearStaleState: removed item for key: ",c),r.remove(c)}),n.push(u)},a=0;au;)n(a,r=e[u++])&&(~o(c,r)||c.push(r));return c}},function(t,e,r){var n=r(9),i=r(1),o=r(35);t.exports=r(8)?Object.defineProperties:function t(e,r){i(e);for(var s,a=o(r),u=a.length,c=0;u>c;)n.f(e,s=a[c++],r[s]);return e}},function(t,e,r){var n=r(16),i=r(38).f,o={}.toString,s="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[];t.exports.f=function t(e){return s&&"[object Window]"==o.call(e)?function(t){try{return i(t)}catch(t){return s.slice()}}(e):i(n(e))}},function(t,e,r){"use strict";var n=r(35),i=r(57),o=r(52),s=r(10),a=r(51),u=Object.assign;t.exports=!u||r(4)(function(){var t={},e={},r=Symbol(),n="abcdefghijklmnopqrst";return t[r]=7,n.split("").forEach(function(t){e[t]=t}),7!=u({},t)[r]||Object.keys(u({},e)).join("")!=n})?function t(e,r){for(var u=s(e),c=arguments.length,f=1,h=i.f,l=o.f;c>f;)for(var p,d=a(arguments[f++]),g=h?n(d).concat(h(d)):n(d),v=g.length,y=0;v>y;)l.call(d,p=g[y++])&&(u[p]=d[p]);return u}:u},function(t,e){t.exports=Object.is||function t(e,r){return e===r?0!==e||1/e==1/r:e!=e&&r!=r}},function(t,e,r){"use strict";var n=r(11),i=r(5),o=r(111),s=[].slice,a={};t.exports=Function.bind||function t(e){var r=n(this),u=s.call(arguments,1),c=function(){var t=u.concat(s.call(arguments));return this instanceof c?function(t,e,r){if(!(e in a)){for(var n=[],i=0;i>>0||(s.test(o)?16:10))}:n},function(t,e,r){var n=r(2).parseFloat,i=r(47).trim;t.exports=1/n(r(78)+"-0")!=-1/0?function t(e){var r=i(String(e),3),o=n(r);return 0===o&&"-"==r.charAt(0)?-0:o}:n},function(t,e,r){var n=r(21);t.exports=function(t,e){if("number"!=typeof t&&"Number"!=n(t))throw TypeError(e);return+t}},function(t,e,r){var n=r(5),i=Math.floor;t.exports=function t(e){return!n(e)&&isFinite(e)&&i(e)===e}},function(t,e){t.exports=Math.log1p||function t(e){return(e=+e)>-1e-8&&e<1e-8?e-e*e/2:Math.log(1+e)}},function(t,e,r){var n=r(81),i=Math.pow,o=i(2,-52),s=i(2,-23),a=i(2,127)*(2-s),u=i(2,-126);t.exports=Math.fround||function t(e){var r,i,c=Math.abs(e),f=n(e);return ca||i!=i?f*(1/0):f*i}},function(t,e,r){var n=r(1);t.exports=function(t,e,r,i){try{return i?e(n(r)[0],r[1]):e(r)}catch(e){var o=t.return;throw void 0!==o&&n(o.call(t)),e}}},function(t,e,r){var n=r(11),i=r(10),o=r(51),s=r(7);t.exports=function(t,e,r,a,u){n(e);var c=i(t),f=o(c),h=s(c.length),l=u?h-1:0,p=u?-1:1;if(r<2)for(;;){if(l in f){a=f[l],l+=p;break}if(l+=p,u?l<0:h<=l)throw TypeError("Reduce of empty array with no initial value")}for(;u?l>=0:h>l;l+=p)l in f&&(a=e(a,f[l],l,c));return a}},function(t,e,r){"use strict";var n=r(10),i=r(36),o=r(7);t.exports=[].copyWithin||function t(e,r){var s=n(this),a=o(s.length),u=i(e,a),c=i(r,a),f=arguments.length>2?arguments[2]:void 0,h=Math.min((void 0===f?a:i(f,a))-c,a-u),l=1;for(c0;)c in s?s[u]=s[c]:delete s[u],u+=l,c+=l;return s}},function(t,e){t.exports=function(t,e){return{value:e,done:!!t}}},function(t,e,r){"use strict";var n=r(93);r(0)({target:"RegExp",proto:!0,forced:n!==/./.exec},{exec:n})},function(t,e,r){r(8)&&"g"!=/./g.flags&&r(9).f(RegExp.prototype,"flags",{configurable:!0,get:r(53)})},function(t,e){t.exports=function(t){try{return{e:!1,v:t()}}catch(t){return{e:!0,v:t}}}},function(t,e,r){var n=r(1),i=r(5),o=r(97);t.exports=function(t,e){if(n(t),i(e)&&e.constructor===t)return e;var r=o.f(t);return(0,r.resolve)(e),r.promise}},function(t,e,r){"use strict";var n=r(127),i=r(43);t.exports=r(65)("Map",function(t){return function e(){return t(this,arguments.length>0?arguments[0]:void 0)}},{get:function t(e){var r=n.getEntry(i(this,"Map"),e);return r&&r.v},set:function t(e,r){return n.def(i(this,"Map"),0===e?0:e,r)}},n,!0)},function(t,e,r){"use strict";var n=r(9).f,i=r(37),o=r(42),s=r(20),a=r(40),u=r(41),c=r(83),f=r(121),h=r(39),l=r(8),p=r(31).fastKey,d=r(43),g=l?"_s":"size",v=function(t,e){var r,n=p(e);if("F"!==n)return t._i[n];for(r=t._f;r;r=r.n)if(r.k==e)return r};t.exports={getConstructor:function(t,e,r,c){var f=t(function(t,n){a(t,f,e,"_i"),t._t=e,t._i=i(null),t._f=void 0,t._l=void 0,t[g]=0,void 0!=n&&u(n,r,t[c],t)});return o(f.prototype,{clear:function t(){for(var r=d(this,e),n=r._i,i=r._f;i;i=i.n)i.r=!0,i.p&&(i.p=i.p.n=void 0),delete n[i.i];r._f=r._l=void 0,r[g]=0},delete:function(t){var r=d(this,e),n=v(r,t);if(n){var i=n.n,o=n.p;delete r._i[n.i],n.r=!0,o&&(o.n=i),i&&(i.p=o),r._f==n&&(r._f=i),r._l==n&&(r._l=o),r[g]--}return!!n},forEach:function t(r){d(this,e);for(var n,i=s(r,arguments.length>1?arguments[1]:void 0,3);n=n?n.n:this._f;)for(i(n.v,n.k,this);n&&n.r;)n=n.p},has:function t(r){return!!v(d(this,e),r)}}),l&&n(f.prototype,"size",{get:function(){return d(this,e)[g]}}),f},def:function(t,e,r){var n,i,o=v(t,e);return o?o.v=r:(t._l=o={i:i=p(e,!0),k:e,v:r,p:n=t._l,n:void 0,r:!1},t._f||(t._f=o),n&&(n.n=o),t[g]++,"F"!==i&&(t._i[i]=o)),t},getEntry:v,setStrong:function(t,e,r){c(t,e,function(t,r){this._t=d(t,e),this._k=r,this._l=void 0},function(){for(var t=this._k,e=this._l;e&&e.r;)e=e.p;return this._t&&(this._l=e=e?e.n:this._t._f)?f(0,"keys"==t?e.k:"values"==t?e.v:[e.k,e.v]):(this._t=void 0,f(1))},r?"entries":"values",!r,!0),h(e)}}},function(t,e,r){"use strict";var n=r(127),i=r(43);t.exports=r(65)("Set",function(t){return function e(){return t(this,arguments.length>0?arguments[0]:void 0)}},{add:function t(e){return n.def(i(this,"Set"),e=0===e?0:e,e)}},n)},function(t,e,r){"use strict";var n,i=r(2),o=r(27)(0),s=r(13),a=r(31),u=r(108),c=r(130),f=r(5),h=r(43),l=r(43),p=!i.ActiveXObject&&"ActiveXObject"in i,d=a.getWeak,g=Object.isExtensible,v=c.ufstore,y=function(t){return function e(){return t(this,arguments.length>0?arguments[0]:void 0)}},m={get:function t(e){if(f(e)){var r=d(e);return!0===r?v(h(this,"WeakMap")).get(e):r?r[this._i]:void 0}},set:function t(e,r){return c.def(h(this,"WeakMap"),e,r)}},_=t.exports=r(65)("WeakMap",y,m,c,!0,!0);l&&p&&(u((n=c.getConstructor(y,"WeakMap")).prototype,m),a.NEED=!0,o(["delete","has","get","set"],function(t){var e=_.prototype,r=e[t];s(e,t,function(e,i){if(f(e)&&!g(e)){this._f||(this._f=new n);var o=this._f[t](e,i);return"set"==t?this:o}return r.call(this,e,i)})}))},function(t,e,r){"use strict";var n=r(42),i=r(31).getWeak,o=r(1),s=r(5),a=r(40),u=r(41),c=r(27),f=r(15),h=r(43),l=c(5),p=c(6),d=0,g=function(t){return t._l||(t._l=new v)},v=function(){this.a=[]},y=function(t,e){return l(t.a,function(t){return t[0]===e})};v.prototype={get:function(t){var e=y(this,t);if(e)return e[1]},has:function(t){return!!y(this,t)},set:function(t,e){var r=y(this,t);r?r[1]=e:this.a.push([t,e])},delete:function(t){var e=p(this.a,function(e){return e[0]===t});return~e&&this.a.splice(e,1),!!~e}},t.exports={getConstructor:function(t,e,r,o){var c=t(function(t,n){a(t,c,e,"_i"),t._t=e,t._i=d++,t._l=void 0,void 0!=n&&u(n,r,t[o],t)});return n(c.prototype,{delete:function(t){if(!s(t))return!1;var r=i(t);return!0===r?g(h(this,e)).delete(t):r&&f(r,this._i)&&delete r[this._i]},has:function t(r){if(!s(r))return!1;var n=i(r);return!0===n?g(h(this,e)).has(r):n&&f(n,this._i)}}),c},def:function(t,e,r){var n=i(o(e),!0);return!0===n?g(t).set(e,r):n[t._i]=r,t},ufstore:g}},function(t,e,r){var n=r(22),i=r(7);t.exports=function(t){if(void 0===t)return 0;var e=n(t),r=i(e);if(e!==r)throw RangeError("Wrong length!");return r}},function(t,e,r){var n=r(38),i=r(57),o=r(1),s=r(2).Reflect;t.exports=s&&s.ownKeys||function t(e){var r=n.f(o(e)),s=i.f;return s?r.concat(s(e)):r}},function(t,e,r){"use strict";var n=r(58),i=r(5),o=r(7),s=r(20),a=r(6)("isConcatSpreadable");t.exports=function t(e,r,u,c,f,h,l,p){for(var d,g,v=f,y=0,m=!!l&&s(l,p,3);y0)v=t(e,r,d,o(d.length),v,h-1)-1;else{if(v>=9007199254740991)throw TypeError();e[v]=d}v++}y++}return v}},function(t,e,r){var n=r(7),i=r(80),o=r(25);t.exports=function(t,e,r,s){var a=String(o(t)),u=a.length,c=void 0===r?" ":String(r),f=n(e);if(f<=u||""==c)return a;var h=f-u,l=i.call(c,Math.ceil(h/c.length));return l.length>h&&(l=l.slice(0,h)),s?l+a:a+l}},function(t,e,r){var n=r(35),i=r(16),o=r(52).f;t.exports=function(t){return function(e){for(var r,s=i(e),a=n(s),u=a.length,c=0,f=[];u>c;)o.call(s,r=a[c++])&&f.push(t?[r,s[r]]:s[r]);return f}}},function(t,e,r){var n=r(46),i=r(137);t.exports=function(t){return function e(){if(n(this)!=t)throw TypeError(t+"#toJSON isn't generic");return i(this)}}},function(t,e,r){var n=r(41);t.exports=function(t,e){var r=[];return n(t,!1,r.push,r,e),r}},function(t,e){t.exports=Math.scale||function t(e,r,n,i,o){return 0===arguments.length||e!=e||r!=r||n!=n||i!=i||o!=o?NaN:e===1/0||e===-1/0?e:(e-r)*(o-i)/(n-r)+i}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.OidcClient=void 0;var n=function(){function t(t,e){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{};!function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),e instanceof o.OidcClientSettings?this._settings=e:this._settings=new o.OidcClientSettings(e)}return t.prototype.createSigninRequest=function t(){var e=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=r.response_type,o=r.scope,s=r.redirect_uri,u=r.data,c=r.state,f=r.prompt,h=r.display,l=r.max_age,p=r.ui_locales,d=r.id_token_hint,g=r.login_hint,v=r.acr_values,y=r.resource,m=r.request,_=r.request_uri,S=r.response_mode,b=r.extraQueryParams,w=r.extraTokenParams,F=r.request_type,E=r.skipUserInfo,x=arguments[1];i.Log.debug("OidcClient.createSigninRequest");var A=this._settings.client_id;n=n||this._settings.response_type,o=o||this._settings.scope,s=s||this._settings.redirect_uri,f=f||this._settings.prompt,h=h||this._settings.display,l=l||this._settings.max_age,p=p||this._settings.ui_locales,v=v||this._settings.acr_values,y=y||this._settings.resource,S=S||this._settings.response_mode,b=b||this._settings.extraQueryParams;var k=this._settings.authority;return a.SigninRequest.isCode(n)&&"code"!==n?Promise.reject(new Error("OpenID Connect hybrid flow is not supported")):this._metadataService.getAuthorizationEndpoint().then(function(t){i.Log.debug("OidcClient.createSigninRequest: Received authorization endpoint",t);var r=new a.SigninRequest({url:t,client_id:A,redirect_uri:s,response_type:n,scope:o,data:u||c,authority:k,prompt:f,display:h,max_age:l,ui_locales:p,id_token_hint:d,login_hint:g,acr_values:v,resource:y,request:m,request_uri:_,extraQueryParams:b,extraTokenParams:w,request_type:F,response_mode:S,client_secret:e._settings.client_secret,skipUserInfo:E}),P=r.state;return(x=x||e._stateStore).set(P.id,P.toStorageString()).then(function(){return r})})},t.prototype.readSigninResponseState=function t(e,r){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];i.Log.debug("OidcClient.readSigninResponseState");var o="query"===this._settings.response_mode||!this._settings.response_mode&&a.SigninRequest.isCode(this._settings.response_type)?"?":"#",s=new u.SigninResponse(e,o);return s.state?(r=r||this._stateStore,(n?r.remove.bind(r):r.get.bind(r))(s.state).then(function(t){if(!t)throw i.Log.error("OidcClient.readSigninResponseState: No matching state found in storage"),new Error("No matching state found in storage");return{state:h.SigninState.fromStorageString(t),response:s}})):(i.Log.error("OidcClient.readSigninResponseState: No state in response"),Promise.reject(new Error("No state in response")))},t.prototype.processSigninResponse=function t(e,r){var n=this;return i.Log.debug("OidcClient.processSigninResponse"),this.readSigninResponseState(e,r,!0).then(function(t){var e=t.state,r=t.response;return i.Log.debug("OidcClient.processSigninResponse: Received state from storage; validating response"),n._validator.validateSigninResponse(e,r)})},t.prototype.createSignoutRequest=function t(){var e=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=r.id_token_hint,o=r.data,s=r.state,a=r.post_logout_redirect_uri,u=r.extraQueryParams,f=r.request_type,h=arguments[1];return i.Log.debug("OidcClient.createSignoutRequest"),a=a||this._settings.post_logout_redirect_uri,u=u||this._settings.extraQueryParams,this._metadataService.getEndSessionEndpoint().then(function(t){if(!t)throw i.Log.error("OidcClient.createSignoutRequest: No end session endpoint url returned"),new Error("no end session endpoint");i.Log.debug("OidcClient.createSignoutRequest: Received end session endpoint",t);var r=new c.SignoutRequest({url:t,id_token_hint:n,post_logout_redirect_uri:a,data:o||s,extraQueryParams:u,request_type:f}),l=r.state;return l&&(i.Log.debug("OidcClient.createSignoutRequest: Signout request has state to persist"),(h=h||e._stateStore).set(l.id,l.toStorageString())),r})},t.prototype.readSignoutResponseState=function t(e,r){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];i.Log.debug("OidcClient.readSignoutResponseState");var o=new f.SignoutResponse(e);if(!o.state)return i.Log.debug("OidcClient.readSignoutResponseState: No state in response"),o.error?(i.Log.warn("OidcClient.readSignoutResponseState: Response was error: ",o.error),Promise.reject(new s.ErrorResponse(o))):Promise.resolve({undefined:void 0,response:o});var a=o.state;return r=r||this._stateStore,(n?r.remove.bind(r):r.get.bind(r))(a).then(function(t){if(!t)throw i.Log.error("OidcClient.readSignoutResponseState: No matching state found in storage"),new Error("No matching state found in storage");return{state:l.State.fromStorageString(t),response:o}})},t.prototype.processSignoutResponse=function t(e,r){var n=this;return i.Log.debug("OidcClient.processSignoutResponse"),this.readSignoutResponseState(e,r,!0).then(function(t){var e=t.state,r=t.response;return e?(i.Log.debug("OidcClient.processSignoutResponse: Received state from storage; validating response"),n._validator.validateSignoutResponse(e,r)):(i.Log.debug("OidcClient.processSignoutResponse: No state from storage; skipping validating response"),r)})},t.prototype.clearStaleState=function t(e){return i.Log.debug("OidcClient.clearStaleState"),e=e||this._stateStore,l.State.clearStaleState(e,this.settings.staleStateAge)},n(t,[{key:"_stateStore",get:function t(){return this.settings.stateStore}},{key:"_validator",get:function t(){return this.settings.validator}},{key:"_metadataService",get:function t(){return this.settings.metadataService}},{key:"settings",get:function t(){return this._settings}},{key:"metadataService",get:function t(){return this._metadataService}}]),t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TokenClient=void 0;var n=r(101),i=r(49),o=r(3);e.TokenClient=function(){function t(e){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:n.JsonService,s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:i.MetadataService;if(function a(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!e)throw o.Log.error("TokenClient.ctor: No settings passed"),new Error("settings");this._settings=e,this._jsonService=new r,this._metadataService=new s(this._settings)}return t.prototype.exchangeCode=function t(){var e=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return(r=Object.assign({},r)).grant_type=r.grant_type||"authorization_code",r.client_id=r.client_id||this._settings.client_id,r.redirect_uri=r.redirect_uri||this._settings.redirect_uri,r.code?r.redirect_uri?r.code_verifier?r.client_id?this._metadataService.getTokenEndpoint(!1).then(function(t){return o.Log.debug("TokenClient.exchangeCode: Received token endpoint"),e._jsonService.postForm(t,r).then(function(t){return o.Log.debug("TokenClient.exchangeCode: response received"),t})}):(o.Log.error("TokenClient.exchangeCode: No client_id passed"),Promise.reject(new Error("A client_id is required"))):(o.Log.error("TokenClient.exchangeCode: No code_verifier passed"),Promise.reject(new Error("A code_verifier is required"))):(o.Log.error("TokenClient.exchangeCode: No redirect_uri passed"),Promise.reject(new Error("A redirect_uri is required"))):(o.Log.error("TokenClient.exchangeCode: No code passed"),Promise.reject(new Error("A code is required")))},t.prototype.exchangeRefreshToken=function t(){var e=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return(r=Object.assign({},r)).grant_type=r.grant_type||"refresh_token",r.client_id=r.client_id||this._settings.client_id,r.client_secret=r.client_secret||this._settings.client_secret,r.refresh_token?r.client_id?this._metadataService.getTokenEndpoint(!1).then(function(t){return o.Log.debug("TokenClient.exchangeRefreshToken: Received token endpoint"),e._jsonService.postForm(t,r).then(function(t){return o.Log.debug("TokenClient.exchangeRefreshToken: response received"),t})}):(o.Log.error("TokenClient.exchangeRefreshToken: No client_id passed"),Promise.reject(new Error("A client_id is required"))):(o.Log.error("TokenClient.exchangeRefreshToken: No refresh_token passed"),Promise.reject(new Error("A refresh_token is required")))},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ErrorResponse=void 0;var n=r(3);e.ErrorResponse=function(t){function e(){var r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},i=r.error,o=r.error_description,s=r.error_uri,a=r.state;if(function u(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,e),!i)throw n.Log.error("No error passed to ErrorResponse"),new Error("error");var c=function f(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,o||i));return c.name="ErrorResponse",c.error=i,c.error_description=o,c.error_uri=s,c.state=a,c}return function r(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e}(Error)},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SigninRequest=void 0;var n=r(3),i=r(55),o=r(143);e.SigninRequest=function(){function t(e){var r=e.url,s=e.client_id,a=e.redirect_uri,u=e.response_type,c=e.scope,f=e.authority,h=e.data,l=e.prompt,p=e.display,d=e.max_age,g=e.ui_locales,v=e.id_token_hint,y=e.login_hint,m=e.acr_values,_=e.resource,S=e.response_mode,b=e.request,w=e.request_uri,F=e.extraQueryParams,E=e.request_type,x=e.client_secret,A=e.extraTokenParams,k=e.skipUserInfo;if(function P(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!r)throw n.Log.error("SigninRequest.ctor: No url passed"),new Error("url");if(!s)throw n.Log.error("SigninRequest.ctor: No client_id passed"),new Error("client_id");if(!a)throw n.Log.error("SigninRequest.ctor: No redirect_uri passed"),new Error("redirect_uri");if(!u)throw n.Log.error("SigninRequest.ctor: No response_type passed"),new Error("response_type");if(!c)throw n.Log.error("SigninRequest.ctor: No scope passed"),new Error("scope");if(!f)throw n.Log.error("SigninRequest.ctor: No authority passed"),new Error("authority");var C=t.isOidc(u),T=t.isCode(u);S||(S=t.isCode(u)?"query":null),this.state=new o.SigninState({nonce:C,data:h,client_id:s,authority:f,redirect_uri:a,code_verifier:T,request_type:E,response_mode:S,client_secret:x,scope:c,extraTokenParams:A,skipUserInfo:k}),r=i.UrlUtility.addQueryParam(r,"client_id",s),r=i.UrlUtility.addQueryParam(r,"redirect_uri",a),r=i.UrlUtility.addQueryParam(r,"response_type",u),r=i.UrlUtility.addQueryParam(r,"scope",c),r=i.UrlUtility.addQueryParam(r,"state",this.state.id),C&&(r=i.UrlUtility.addQueryParam(r,"nonce",this.state.nonce)),T&&(r=i.UrlUtility.addQueryParam(r,"code_challenge",this.state.code_challenge),r=i.UrlUtility.addQueryParam(r,"code_challenge_method","S256"));var R={prompt:l,display:p,max_age:d,ui_locales:g,id_token_hint:v,login_hint:y,acr_values:m,resource:_,request:b,request_uri:w,response_mode:S};for(var I in R)R[I]&&(r=i.UrlUtility.addQueryParam(r,I,R[I]));for(var O in F)r=i.UrlUtility.addQueryParam(r,O,F[O]);this.url=r}return t.isOidc=function t(e){return!!e.split(/\s+/g).filter(function(t){return"id_token"===t})[0]},t.isOAuth=function t(e){return!!e.split(/\s+/g).filter(function(t){return"token"===t})[0]},t.isCode=function t(e){return!!e.split(/\s+/g).filter(function(t){return"code"===t})[0]},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SigninState=void 0;var n=function(){function t(t,e){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{},n=r.nonce,i=r.authority,o=r.client_id,u=r.redirect_uri,c=r.code_verifier,f=r.response_mode,h=r.client_secret,l=r.scope,p=r.extraTokenParams,d=r.skipUserInfo;!function g(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,e);var v=function y(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,arguments[0]));if(!0===n?v._nonce=(0,a.default)():n&&(v._nonce=n),!0===c?v._code_verifier=(0,a.default)()+(0,a.default)()+(0,a.default)():c&&(v._code_verifier=c),v.code_verifier){var m=s.JoseUtil.hashString(v.code_verifier,"SHA256");v._code_challenge=s.JoseUtil.hexToBase64Url(m)}return v._redirect_uri=u,v._authority=i,v._client_id=o,v._response_mode=f,v._client_secret=h,v._scope=l,v._extraTokenParams=p,v._skipUserInfo=d,v}return function r(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype.toStorageString=function t(){return i.Log.debug("SigninState.toStorageString"),JSON.stringify({id:this.id,data:this.data,created:this.created,request_type:this.request_type,nonce:this.nonce,code_verifier:this.code_verifier,redirect_uri:this.redirect_uri,authority:this.authority,client_id:this.client_id,response_mode:this.response_mode,client_secret:this.client_secret,scope:this.scope,extraTokenParams:this.extraTokenParams,skipUserInfo:this.skipUserInfo})},e.fromStorageString=function t(r){return i.Log.debug("SigninState.fromStorageString"),new e(JSON.parse(r))},n(e,[{key:"nonce",get:function t(){return this._nonce}},{key:"authority",get:function t(){return this._authority}},{key:"client_id",get:function t(){return this._client_id}},{key:"redirect_uri",get:function t(){return this._redirect_uri}},{key:"code_verifier",get:function t(){return this._code_verifier}},{key:"code_challenge",get:function t(){return this._code_challenge}},{key:"response_mode",get:function t(){return this._response_mode}},{key:"client_secret",get:function t(){return this._client_secret}},{key:"scope",get:function t(){return this._scope}},{key:"extraTokenParams",get:function t(){return this._extraTokenParams}},{key:"skipUserInfo",get:function t(){return this._skipUserInfo}}]),e}(o.State)},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.default=function n(){return(0,i.default)().replace(/-/g,"")};var i=function o(t){return t&&t.__esModule?t:{default:t}}(r(365));t.exports=e.default},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.User=void 0;var n=function(){function t(t,e){for(var r=0;r0){var n=parseInt(Date.now()/1e3);this.expires_at=n+r}}},{key:"expired",get:function t(){var e=this.expires_in;if(void 0!==e)return e<=0}},{key:"scopes",get:function t(){return(this.scope||"").split(" ")}}]),t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AccessTokenEvents=void 0;var n=r(3),i=r(380);var o=60;e.AccessTokenEvents=function(){function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=e.accessTokenExpiringNotificationTime,n=void 0===r?o:r,s=e.accessTokenExpiringTimer,a=void 0===s?new i.Timer("Access token expiring"):s,u=e.accessTokenExpiredTimer,c=void 0===u?new i.Timer("Access token expired"):u;!function f(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this._accessTokenExpiringNotificationTime=n,this._accessTokenExpiring=a,this._accessTokenExpired=c}return t.prototype.load=function t(e){if(e.access_token&&void 0!==e.expires_in){var r=e.expires_in;if(n.Log.debug("AccessTokenEvents.load: access token present, remaining duration:",r),r>0){var i=r-this._accessTokenExpiringNotificationTime;i<=0&&(i=1),n.Log.debug("AccessTokenEvents.load: registering expiring timer in:",i),this._accessTokenExpiring.init(i)}else n.Log.debug("AccessTokenEvents.load: canceling existing expiring timer becase we're past expiration."),this._accessTokenExpiring.cancel();var o=r+1;n.Log.debug("AccessTokenEvents.load: registering expired timer in:",o),this._accessTokenExpired.init(o)}else this._accessTokenExpiring.cancel(),this._accessTokenExpired.cancel()},t.prototype.unload=function t(){n.Log.debug("AccessTokenEvents.unload: canceling existing access token timers"),this._accessTokenExpiring.cancel(),this._accessTokenExpired.cancel()},t.prototype.addAccessTokenExpiring=function t(e){this._accessTokenExpiring.addHandler(e)},t.prototype.removeAccessTokenExpiring=function t(e){this._accessTokenExpiring.removeHandler(e)},t.prototype.addAccessTokenExpired=function t(e){this._accessTokenExpired.addHandler(e)},t.prototype.removeAccessTokenExpired=function t(e){this._accessTokenExpired.removeHandler(e)},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Event=void 0;var n=r(3);e.Event=function(){function t(e){!function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this._name=e,this._callbacks=[]}return t.prototype.addHandler=function t(e){this._callbacks.push(e)},t.prototype.removeHandler=function t(e){var r=this._callbacks.findIndex(function(t){return t===e});r>=0&&this._callbacks.splice(r,1)},t.prototype.raise=function t(){n.Log.debug("Event: Raising event: "+this._name);for(var e=0;e1&&void 0!==arguments[1]?arguments[1]:o.CheckSessionIFrame;if(function s(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!e)throw i.Log.error("SessionMonitor.ctor: No user manager passed to SessionMonitor"),new Error("userManager");this._userManager=e,this._CheckSessionIFrameCtor=n,this._userManager.events.addUserLoaded(this._start.bind(this)),this._userManager.events.addUserUnloaded(this._stop.bind(this)),this._userManager.getUser().then(function(t){t&&r._start(t)}).catch(function(t){i.Log.error("SessionMonitor ctor: error from getUser:",t.message)})}return t.prototype._start=function t(e){var r=this,n=e.session_state;n&&(this._sub=e.profile.sub,this._sid=e.profile.sid,i.Log.debug("SessionMonitor._start: session_state:",n,", sub:",this._sub),this._checkSessionIFrame?this._checkSessionIFrame.start(n):this._metadataService.getCheckSessionIframe().then(function(t){if(t){i.Log.debug("SessionMonitor._start: Initializing check session iframe");var e=r._client_id,o=r._checkSessionInterval,s=r._stopCheckSessionOnError;r._checkSessionIFrame=new r._CheckSessionIFrameCtor(r._callback.bind(r),e,t,o,s),r._checkSessionIFrame.load().then(function(){r._checkSessionIFrame.start(n)})}else i.Log.warn("SessionMonitor._start: No check session iframe found in the metadata")}).catch(function(t){i.Log.error("SessionMonitor._start: Error from getCheckSessionIframe:",t.message)}))},t.prototype._stop=function t(){this._sub=null,this._sid=null,this._checkSessionIFrame&&(i.Log.debug("SessionMonitor._stop"),this._checkSessionIFrame.stop())},t.prototype._callback=function t(){var e=this;this._userManager.querySessionStatus().then(function(t){var r=!0;t?t.sub===e._sub?(r=!1,e._checkSessionIFrame.start(t.session_state),t.sid===e._sid?i.Log.debug("SessionMonitor._callback: Same sub still logged in at OP, restarting check session iframe; session_state:",t.session_state):(i.Log.debug("SessionMonitor._callback: Same sub still logged in at OP, session state has changed, restarting check session iframe; session_state:",t.session_state),e._userManager.events._raiseUserSessionChanged())):i.Log.debug("SessionMonitor._callback: Different subject signed into OP:",t.sub):i.Log.debug("SessionMonitor._callback: Subject no longer signed into OP"),r&&(i.Log.debug("SessionMonitor._callback: SessionMonitor._callback; raising signed out event"),e._userManager.events._raiseUserSignedOut())}).catch(function(t){i.Log.debug("SessionMonitor._callback: Error calling queryCurrentSigninSession; raising signed out event",t.message),e._userManager.events._raiseUserSignedOut()})},n(t,[{key:"_settings",get:function t(){return this._userManager.settings}},{key:"_metadataService",get:function t(){return this._userManager.metadataService}},{key:"_client_id",get:function t(){return this._settings.client_id}},{key:"_checkSessionInterval",get:function t(){return this._settings.checkSessionInterval}},{key:"_stopCheckSessionOnError",get:function t(){return this._settings.stopCheckSessionOnError}}]),t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CheckSessionIFrame=void 0;var n=r(3);var i=2e3;e.CheckSessionIFrame=function(){function t(e,r,n,o){var s=!(arguments.length>4&&void 0!==arguments[4])||arguments[4];!function a(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this._callback=e,this._client_id=r,this._url=n,this._interval=o||i,this._stopOnError=s;var u=n.indexOf("/",n.indexOf("//")+2);this._frame_origin=n.substr(0,u),this._frame=window.document.createElement("iframe"),this._frame.style.visibility="hidden",this._frame.style.position="absolute",this._frame.style.display="none",this._frame.style.width=0,this._frame.style.height=0,this._frame.src=n}return t.prototype.load=function t(){var e=this;return new Promise(function(t){e._frame.onload=function(){t()},window.document.body.appendChild(e._frame),e._boundMessageEvent=e._message.bind(e),window.addEventListener("message",e._boundMessageEvent,!1)})},t.prototype._message=function t(e){e.origin===this._frame_origin&&e.source===this._frame.contentWindow&&("error"===e.data?(n.Log.error("CheckSessionIFrame: error message from check session op iframe"),this._stopOnError&&this.stop()):"changed"===e.data?(n.Log.debug("CheckSessionIFrame: changed message from check session op iframe"),this.stop(),this._callback()):n.Log.debug("CheckSessionIFrame: "+e.data+" message from check session op iframe"))},t.prototype.start=function t(e){var r=this;if(this._session_state!==e){n.Log.debug("CheckSessionIFrame.start"),this.stop(),this._session_state=e;var i=function t(){r._frame.contentWindow.postMessage(r._client_id+" "+r._session_state,r._frame_origin)};i(),this._timer=window.setInterval(i,this._interval)}},t.prototype.stop=function t(){this._session_state=null,this._timer&&(n.Log.debug("CheckSessionIFrame.stop"),window.clearInterval(this._timer),this._timer=null)},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TokenRevocationClient=void 0;var n=r(3),i=r(49),o=r(44);e.TokenRevocationClient=function(){function t(e){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:o.Global.XMLHttpRequest,s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:i.MetadataService;if(function a(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!e)throw n.Log.error("TokenRevocationClient.ctor: No settings provided"),new Error("No settings provided.");this._settings=e,this._XMLHttpRequestCtor=r,this._metadataService=new s(this._settings)}return t.prototype.revoke=function t(e,r){var i=this,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"access_token";if(!e)throw n.Log.error("TokenRevocationClient.revoke: No token provided"),new Error("No token provided.");if("access_token"!==o&&"refresh_token"!=o)throw n.Log.error("TokenRevocationClient.revoke: Invalid token type"),new Error("Invalid token type.");return this._metadataService.getRevocationEndpoint().then(function(t){if(t){n.Log.debug("TokenRevocationClient.revoke: Revoking "+o);var s=i._settings.client_id,a=i._settings.client_secret;return i._revoke(t,s,a,e,o)}if(r)throw n.Log.error("TokenRevocationClient.revoke: Revocation not supported"),new Error("Revocation not supported")})},t.prototype._revoke=function t(e,r,i,o,s){var a=this;return new Promise(function(t,u){var c=new a._XMLHttpRequestCtor;c.open("POST",e),c.onload=function(){n.Log.debug("TokenRevocationClient.revoke: HTTP response received, status",c.status),200===c.status?t():u(Error(c.statusText+" ("+c.status+")"))},c.onerror=function(){n.Log.debug("TokenRevocationClient.revoke: Network Error."),u("Network Error")};var f="client_id="+encodeURIComponent(r);i&&(f+="&client_secret="+encodeURIComponent(i)),f+="&token_type_hint="+encodeURIComponent(s),f+="&token="+encodeURIComponent(o),c.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),c.send(f)})},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CordovaPopupWindow=void 0;var n=function(){function t(t,e){for(var r=0;ro;)z(e,n=i[o++],r[n]);return e},G=function t(e){var r=L.call(this,e=b(e,!0));return!(this===B&&i(j,e)&&!i(U,e))&&(!(r||!i(this,e)||!i(j,e)||i(this,D)&&this[D][e])||r)},X=function t(e,r){if(e=S(e),r=b(r,!0),e!==B||!i(j,r)||i(U,r)){var n=P(e,r);return!n||!i(j,r)||i(e,D)&&e[D][r]||(n.enumerable=!0),n}},$=function t(e){for(var r,n=T(S(e)),o=[],s=0;n.length>s;)i(j,r=n[s++])||r==D||r==u||o.push(r);return o},Q=function t(e){for(var r,n=e===B,o=T(n?U:S(e)),s=[],a=0;o.length>a;)!i(j,r=o[a++])||n&&!i(B,r)||s.push(j[r]);return s};H||(a((R=function t(){if(this instanceof R)throw TypeError("Symbol is not a constructor!");var e=l(arguments.length>0?arguments[0]:void 0),r=function(t){this===B&&r.call(U,t),i(this,D)&&i(this[D],e)&&(this[D][e]=!1),q(this,e,w(1,t))};return o&&K&&q(B,e,{configurable:!0,set:r}),W(e)}).prototype,"toString",function t(){return this._k}),x.f=X,A.f=z,r(38).f=E.f=$,r(52).f=G,r(57).f=Q,o&&!r(30)&&a(B,"propertyIsEnumerable",G,!0),d.f=function(t){return W(p(t))}),s(s.G+s.W+s.F*!H,{Symbol:R});for(var Z="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),tt=0;Z.length>tt;)p(Z[tt++]);for(var et=k(p.store),rt=0;et.length>rt;)g(et[rt++]);s(s.S+s.F*!H,"Symbol",{for:function(t){return i(M,t+="")?M[t]:M[t]=R(t)},keyFor:function t(e){if(!J(e))throw TypeError(e+" is not a symbol!");for(var r in M)if(M[r]===e)return r},useSetter:function(){K=!0},useSimple:function(){K=!1}}),s(s.S+s.F*!H,"Object",{create:function t(e,r){return void 0===r?F(e):Y(F(e),r)},defineProperty:z,defineProperties:Y,getOwnPropertyDescriptor:X,getOwnPropertyNames:$,getOwnPropertySymbols:Q}),I&&s(s.S+s.F*(!H||c(function(){var t=R();return"[null]"!=O([t])||"{}"!=O({a:t})||"{}"!=O(Object(t))})),"JSON",{stringify:function t(e){for(var r,n,i=[e],o=1;arguments.length>o;)i.push(arguments[o++]);if(n=r=i[1],(_(r)||void 0!==e)&&!J(e))return y(r)||(r=function(t,e){if("function"==typeof n&&(e=n.call(this,t,e)),!J(e))return e}),i[1]=r,O.apply(I,i)}}),R.prototype[N]||r(12)(R.prototype,N,R.prototype.valueOf),h(R,"Symbol"),h(Math,"Math",!0),h(n.JSON,"JSON",!0)},function(t,e,r){t.exports=r(50)("native-function-to-string",Function.toString)},function(t,e,r){var n=r(35),i=r(57),o=r(52);t.exports=function(t){var e=n(t),r=i.f;if(r)for(var s,a=r(t),u=o.f,c=0;a.length>c;)u.call(t,s=a[c++])&&e.push(s);return e}},function(t,e,r){var n=r(0);n(n.S,"Object",{create:r(37)})},function(t,e,r){var n=r(0);n(n.S+n.F*!r(8),"Object",{defineProperty:r(9).f})},function(t,e,r){var n=r(0);n(n.S+n.F*!r(8),"Object",{defineProperties:r(106)})},function(t,e,r){var n=r(16),i=r(17).f;r(26)("getOwnPropertyDescriptor",function(){return function t(e,r){return i(n(e),r)}})},function(t,e,r){var n=r(10),i=r(18);r(26)("getPrototypeOf",function(){return function t(e){return i(n(e))}})},function(t,e,r){var n=r(10),i=r(35);r(26)("keys",function(){return function t(e){return i(n(e))}})},function(t,e,r){r(26)("getOwnPropertyNames",function(){return r(107).f})},function(t,e,r){var n=r(5),i=r(31).onFreeze;r(26)("freeze",function(t){return function e(r){return t&&n(r)?t(i(r)):r}})},function(t,e,r){var n=r(5),i=r(31).onFreeze;r(26)("seal",function(t){return function e(r){return t&&n(r)?t(i(r)):r}})},function(t,e,r){var n=r(5),i=r(31).onFreeze;r(26)("preventExtensions",function(t){return function e(r){return t&&n(r)?t(i(r)):r}})},function(t,e,r){var n=r(5);r(26)("isFrozen",function(t){return function e(r){return!n(r)||!!t&&t(r)}})},function(t,e,r){var n=r(5);r(26)("isSealed",function(t){return function e(r){return!n(r)||!!t&&t(r)}})},function(t,e,r){var n=r(5);r(26)("isExtensible",function(t){return function e(r){return!!n(r)&&(!t||t(r))}})},function(t,e,r){var n=r(0);n(n.S+n.F,"Object",{assign:r(108)})},function(t,e,r){var n=r(0);n(n.S,"Object",{is:r(109)})},function(t,e,r){var n=r(0);n(n.S,"Object",{setPrototypeOf:r(77).set})},function(t,e,r){"use strict";var n=r(46),i={};i[r(6)("toStringTag")]="z",i+""!="[object z]"&&r(13)(Object.prototype,"toString",function t(){return"[object "+n(this)+"]"},!0)},function(t,e,r){var n=r(0);n(n.P,"Function",{bind:r(110)})},function(t,e,r){var n=r(9).f,i=Function.prototype,o=/^\s*function ([^ (]*)/;"name"in i||r(8)&&n(i,"name",{configurable:!0,get:function(){try{return(""+this).match(o)[1]}catch(t){return""}}})},function(t,e,r){"use strict";var n=r(5),i=r(18),o=r(6)("hasInstance"),s=Function.prototype;o in s||r(9).f(s,o,{value:function(t){if("function"!=typeof this||!n(t))return!1;if(!n(this.prototype))return t instanceof this;for(;t=i(t);)if(this.prototype===t)return!0;return!1}})},function(t,e,r){var n=r(0),i=r(112);n(n.G+n.F*(parseInt!=i),{parseInt:i})},function(t,e,r){var n=r(0),i=r(113);n(n.G+n.F*(parseFloat!=i),{parseFloat:i})},function(t,e,r){"use strict";var n=r(2),i=r(15),o=r(21),s=r(79),a=r(24),u=r(4),c=r(38).f,f=r(17).f,h=r(9).f,l=r(47).trim,p=n.Number,d=p,g=p.prototype,v="Number"==o(r(37)(g)),y="trim"in String.prototype,m=function(t){var e=a(t,!1);if("string"==typeof e&&e.length>2){var r,n,i,o=(e=y?e.trim():l(e,3)).charCodeAt(0);if(43===o||45===o){if(88===(r=e.charCodeAt(2))||120===r)return NaN}else if(48===o){switch(e.charCodeAt(1)){case 66:case 98:n=2,i=49;break;case 79:case 111:n=8,i=55;break;default:return+e}for(var s,u=e.slice(2),c=0,f=u.length;ci)return NaN;return parseInt(u,n)}}return+e};if(!p(" 0o1")||!p("0b1")||p("+0x1")){p=function t(e){var r=arguments.length<1?0:e,n=this;return n instanceof p&&(v?u(function(){g.valueOf.call(n)}):"Number"!=o(n))?s(new d(m(r)),n,p):m(r)};for(var _,S=r(8)?c(d):"MAX_VALUE,MIN_VALUE,NaN,NEGATIVE_INFINITY,POSITIVE_INFINITY,EPSILON,isFinite,isInteger,isNaN,isSafeInteger,MAX_SAFE_INTEGER,MIN_SAFE_INTEGER,parseFloat,parseInt,isInteger".split(","),b=0;S.length>b;b++)i(d,_=S[b])&&!i(p,_)&&h(p,_,f(d,_));p.prototype=g,g.constructor=p,r(13)(n,"Number",p)}},function(t,e,r){"use strict";var n=r(0),i=r(22),o=r(114),s=r(80),a=1..toFixed,u=Math.floor,c=[0,0,0,0,0,0],f="Number.toFixed: incorrect invocation!",h=function(t,e){for(var r=-1,n=e;++r<6;)n+=t*c[r],c[r]=n%1e7,n=u(n/1e7)},l=function(t){for(var e=6,r=0;--e>=0;)r+=c[e],c[e]=u(r/t),r=r%t*1e7},p=function(){for(var t=6,e="";--t>=0;)if(""!==e||0===t||0!==c[t]){var r=String(c[t]);e=""===e?r:e+s.call("0",7-r.length)+r}return e},d=function(t,e,r){return 0===e?r:e%2==1?d(t,e-1,r*t):d(t*t,e/2,r)};n(n.P+n.F*(!!a&&("0.000"!==8e-5.toFixed(3)||"1"!==.9.toFixed(0)||"1.25"!==1.255.toFixed(2)||"1000000000000000128"!==(0xde0b6b3a7640080).toFixed(0))||!r(4)(function(){a.call({})})),"Number",{toFixed:function t(e){var r,n,a,u,c=o(this,f),g=i(e),v="",y="0";if(g<0||g>20)throw RangeError(f);if(c!=c)return"NaN";if(c<=-1e21||c>=1e21)return String(c);if(c<0&&(v="-",c=-c),c>1e-21)if(n=(r=function(t){for(var e=0,r=t;r>=4096;)e+=12,r/=4096;for(;r>=2;)e+=1,r/=2;return e}(c*d(2,69,1))-69)<0?c*d(2,-r,1):c/d(2,r,1),n*=4503599627370496,(r=52-r)>0){for(h(0,n),a=g;a>=7;)h(1e7,0),a-=7;for(h(d(10,a,1),0),a=r-1;a>=23;)l(1<<23),a-=23;l(1<0?v+((u=y.length)<=g?"0."+s.call("0",g-u)+y:y.slice(0,u-g)+"."+y.slice(u-g)):v+y}})},function(t,e,r){"use strict";var n=r(0),i=r(4),o=r(114),s=1..toPrecision;n(n.P+n.F*(i(function(){return"1"!==s.call(1,void 0)})||!i(function(){s.call({})})),"Number",{toPrecision:function t(e){var r=o(this,"Number#toPrecision: incorrect invocation!");return void 0===e?s.call(r):s.call(r,e)}})},function(t,e,r){var n=r(0);n(n.S,"Number",{EPSILON:Math.pow(2,-52)})},function(t,e,r){var n=r(0),i=r(2).isFinite;n(n.S,"Number",{isFinite:function t(e){return"number"==typeof e&&i(e)}})},function(t,e,r){var n=r(0);n(n.S,"Number",{isInteger:r(115)})},function(t,e,r){var n=r(0);n(n.S,"Number",{isNaN:function t(e){return e!=e}})},function(t,e,r){var n=r(0),i=r(115),o=Math.abs;n(n.S,"Number",{isSafeInteger:function t(e){return i(e)&&o(e)<=9007199254740991}})},function(t,e,r){var n=r(0);n(n.S,"Number",{MAX_SAFE_INTEGER:9007199254740991})},function(t,e,r){var n=r(0);n(n.S,"Number",{MIN_SAFE_INTEGER:-9007199254740991})},function(t,e,r){var n=r(0),i=r(113);n(n.S+n.F*(Number.parseFloat!=i),"Number",{parseFloat:i})},function(t,e,r){var n=r(0),i=r(112);n(n.S+n.F*(Number.parseInt!=i),"Number",{parseInt:i})},function(t,e,r){var n=r(0),i=r(116),o=Math.sqrt,s=Math.acosh;n(n.S+n.F*!(s&&710==Math.floor(s(Number.MAX_VALUE))&&s(1/0)==1/0),"Math",{acosh:function t(e){return(e=+e)<1?NaN:e>94906265.62425156?Math.log(e)+Math.LN2:i(e-1+o(e-1)*o(e+1))}})},function(t,e,r){var n=r(0),i=Math.asinh;n(n.S+n.F*!(i&&1/i(0)>0),"Math",{asinh:function t(e){return isFinite(e=+e)&&0!=e?e<0?-t(-e):Math.log(e+Math.sqrt(e*e+1)):e}})},function(t,e,r){var n=r(0),i=Math.atanh;n(n.S+n.F*!(i&&1/i(-0)<0),"Math",{atanh:function t(e){return 0==(e=+e)?e:Math.log((1+e)/(1-e))/2}})},function(t,e,r){var n=r(0),i=r(81);n(n.S,"Math",{cbrt:function t(e){return i(e=+e)*Math.pow(Math.abs(e),1/3)}})},function(t,e,r){var n=r(0);n(n.S,"Math",{clz32:function t(e){return(e>>>=0)?31-Math.floor(Math.log(e+.5)*Math.LOG2E):32}})},function(t,e,r){var n=r(0),i=Math.exp;n(n.S,"Math",{cosh:function t(e){return(i(e=+e)+i(-e))/2}})},function(t,e,r){var n=r(0),i=r(82);n(n.S+n.F*(i!=Math.expm1),"Math",{expm1:i})},function(t,e,r){var n=r(0);n(n.S,"Math",{fround:r(117)})},function(t,e,r){var n=r(0),i=Math.abs;n(n.S,"Math",{hypot:function t(e,r){for(var n,o,s=0,a=0,u=arguments.length,c=0;a0?(o=n/c)*o:n;return c===1/0?1/0:c*Math.sqrt(s)}})},function(t,e,r){var n=r(0),i=Math.imul;n(n.S+n.F*r(4)(function(){return-5!=i(4294967295,5)||2!=i.length}),"Math",{imul:function t(e,r){var n=+e,i=+r,o=65535&n,s=65535&i;return 0|o*s+((65535&n>>>16)*s+o*(65535&i>>>16)<<16>>>0)}})},function(t,e,r){var n=r(0);n(n.S,"Math",{log10:function t(e){return Math.log(e)*Math.LOG10E}})},function(t,e,r){var n=r(0);n(n.S,"Math",{log1p:r(116)})},function(t,e,r){var n=r(0);n(n.S,"Math",{log2:function t(e){return Math.log(e)/Math.LN2}})},function(t,e,r){var n=r(0);n(n.S,"Math",{sign:r(81)})},function(t,e,r){var n=r(0),i=r(82),o=Math.exp;n(n.S+n.F*r(4)(function(){return-2e-17!=!Math.sinh(-2e-17)}),"Math",{sinh:function t(e){return Math.abs(e=+e)<1?(i(e)-i(-e))/2:(o(e-1)-o(-e-1))*(Math.E/2)}})},function(t,e,r){var n=r(0),i=r(82),o=Math.exp;n(n.S,"Math",{tanh:function t(e){var r=i(e=+e),n=i(-e);return r==1/0?1:n==1/0?-1:(r-n)/(o(e)+o(-e))}})},function(t,e,r){var n=r(0);n(n.S,"Math",{trunc:function t(e){return(e>0?Math.floor:Math.ceil)(e)}})},function(t,e,r){var n=r(0),i=r(36),o=String.fromCharCode,s=String.fromCodePoint;n(n.S+n.F*(!!s&&1!=s.length),"String",{fromCodePoint:function t(e){for(var r,n=[],s=arguments.length,a=0;s>a;){if(r=+arguments[a++],i(r,1114111)!==r)throw RangeError(r+" is not a valid code point");n.push(r<65536?o(r):o(55296+((r-=65536)>>10),r%1024+56320))}return n.join("")}})},function(t,e,r){var n=r(0),i=r(16),o=r(7);n(n.S,"String",{raw:function t(e){for(var r=i(e.raw),n=o(r.length),s=arguments.length,a=[],u=0;n>u;)a.push(String(r[u++])),u=e.length?{value:void 0,done:!0}:(t=n(e,r),this._i+=t.length,{value:t,done:!1})})},function(t,e,r){"use strict";var n=r(0),i=r(59)(!1);n(n.P,"String",{codePointAt:function t(e){return i(this,e)}})},function(t,e,r){"use strict";var n=r(0),i=r(7),o=r(85),s="".endsWith;n(n.P+n.F*r(86)("endsWith"),"String",{endsWith:function t(e){var r=o(this,e,"endsWith"),n=arguments.length>1?arguments[1]:void 0,a=i(r.length),u=void 0===n?a:Math.min(i(n),a),c=String(e);return s?s.call(r,c,u):r.slice(u-c.length,u)===c}})},function(t,e,r){"use strict";var n=r(0),i=r(85);n(n.P+n.F*r(86)("includes"),"String",{includes:function t(e){return!!~i(this,e,"includes").indexOf(e,arguments.length>1?arguments[1]:void 0)}})},function(t,e,r){var n=r(0);n(n.P,"String",{repeat:r(80)})},function(t,e,r){"use strict";var n=r(0),i=r(7),o=r(85),s="".startsWith;n(n.P+n.F*r(86)("startsWith"),"String",{startsWith:function t(e){var r=o(this,e,"startsWith"),n=i(Math.min(arguments.length>1?arguments[1]:void 0,r.length)),a=String(e);return s?s.call(r,a,n):r.slice(n,n+a.length)===a}})},function(t,e,r){"use strict";r(14)("anchor",function(t){return function e(r){return t(this,"a","name",r)}})},function(t,e,r){"use strict";r(14)("big",function(t){return function e(){return t(this,"big","","")}})},function(t,e,r){"use strict";r(14)("blink",function(t){return function e(){return t(this,"blink","","")}})},function(t,e,r){"use strict";r(14)("bold",function(t){return function e(){return t(this,"b","","")}})},function(t,e,r){"use strict";r(14)("fixed",function(t){return function e(){return t(this,"tt","","")}})},function(t,e,r){"use strict";r(14)("fontcolor",function(t){return function e(r){return t(this,"font","color",r)}})},function(t,e,r){"use strict";r(14)("fontsize",function(t){return function e(r){return t(this,"font","size",r)}})},function(t,e,r){"use strict";r(14)("italics",function(t){return function e(){return t(this,"i","","")}})},function(t,e,r){"use strict";r(14)("link",function(t){return function e(r){return t(this,"a","href",r)}})},function(t,e,r){"use strict";r(14)("small",function(t){return function e(){return t(this,"small","","")}})},function(t,e,r){"use strict";r(14)("strike",function(t){return function e(){return t(this,"strike","","")}})},function(t,e,r){"use strict";r(14)("sub",function(t){return function e(){return t(this,"sub","","")}})},function(t,e,r){"use strict";r(14)("sup",function(t){return function e(){return t(this,"sup","","")}})},function(t,e,r){var n=r(0);n(n.S,"Date",{now:function(){return(new Date).getTime()}})},function(t,e,r){"use strict";var n=r(0),i=r(10),o=r(24);n(n.P+n.F*r(4)(function(){return null!==new Date(NaN).toJSON()||1!==Date.prototype.toJSON.call({toISOString:function(){return 1}})}),"Date",{toJSON:function t(e){var r=i(this),n=o(r);return"number"!=typeof n||isFinite(n)?r.toISOString():null}})},function(t,e,r){var n=r(0),i=r(234);n(n.P+n.F*(Date.prototype.toISOString!==i),"Date",{toISOString:i})},function(t,e,r){"use strict";var n=r(4),i=Date.prototype.getTime,o=Date.prototype.toISOString,s=function(t){return t>9?t:"0"+t};t.exports=n(function(){return"0385-07-25T07:06:39.999Z"!=o.call(new Date(-5e13-1))})||!n(function(){o.call(new Date(NaN))})?function t(){if(!isFinite(i.call(this)))throw RangeError("Invalid time value");var e=this,r=e.getUTCFullYear(),n=e.getUTCMilliseconds(),o=r<0?"-":r>9999?"+":"";return o+("00000"+Math.abs(r)).slice(o?-6:-4)+"-"+s(e.getUTCMonth()+1)+"-"+s(e.getUTCDate())+"T"+s(e.getUTCHours())+":"+s(e.getUTCMinutes())+":"+s(e.getUTCSeconds())+"."+(n>99?n:"0"+s(n))+"Z"}:o},function(t,e,r){var n=Date.prototype,i=n.toString,o=n.getTime;new Date(NaN)+""!="Invalid Date"&&r(13)(n,"toString",function t(){var e=o.call(this);return e==e?i.call(this):"Invalid Date"})},function(t,e,r){var n=r(6)("toPrimitive"),i=Date.prototype;n in i||r(12)(i,n,r(237))},function(t,e,r){"use strict";var n=r(1),i=r(24);t.exports=function(t){if("string"!==t&&"number"!==t&&"default"!==t)throw TypeError("Incorrect hint");return i(n(this),"number"!=t)}},function(t,e,r){var n=r(0);n(n.S,"Array",{isArray:r(58)})},function(t,e,r){"use strict";var n=r(20),i=r(0),o=r(10),s=r(118),a=r(87),u=r(7),c=r(88),f=r(89);i(i.S+i.F*!r(61)(function(t){Array.from(t)}),"Array",{from:function t(e){var r,i,h,l,p=o(e),d="function"==typeof this?this:Array,g=arguments.length,v=g>1?arguments[1]:void 0,y=void 0!==v,m=0,_=f(p);if(y&&(v=n(v,g>2?arguments[2]:void 0,2)),void 0==_||d==Array&&a(_))for(i=new d(r=u(p.length));r>m;m++)c(i,m,y?v(p[m],m):p[m]);else for(l=_.call(p),i=new d;!(h=l.next()).done;m++)c(i,m,y?s(l,v,[h.value,m],!0):h.value);return i.length=m,i}})},function(t,e,r){"use strict";var n=r(0),i=r(88);n(n.S+n.F*r(4)(function(){function t(){}return!(Array.of.call(t)instanceof t)}),"Array",{of:function t(){for(var e=0,r=arguments.length,n=new("function"==typeof this?this:Array)(r);r>e;)i(n,e,arguments[e++]);return n.length=r,n}})},function(t,e,r){"use strict";var n=r(0),i=r(16),o=[].join;n(n.P+n.F*(r(51)!=Object||!r(23)(o)),"Array",{join:function t(e){return o.call(i(this),void 0===e?",":e)}})},function(t,e,r){"use strict";var n=r(0),i=r(76),o=r(21),s=r(36),a=r(7),u=[].slice;n(n.P+n.F*r(4)(function(){i&&u.call(i)}),"Array",{slice:function t(e,r){var n=a(this.length),i=o(this);if(r=void 0===r?n:r,"Array"==i)return u.call(this,e,r);for(var c=s(e,n),f=s(r,n),h=a(f-c),l=new Array(h),p=0;p1&&(c=Math.min(c,o(arguments[1]))),c<0&&(c=n+c);c>=0;c--)if(c in r&&r[c]===e)return c||0;return-1}})},function(t,e,r){var n=r(0);n(n.P,"Array",{copyWithin:r(120)}),r(32)("copyWithin")},function(t,e,r){var n=r(0);n(n.P,"Array",{fill:r(91)}),r(32)("fill")},function(t,e,r){"use strict";var n=r(0),i=r(27)(5),o=!0;"find"in[]&&Array(1).find(function(){o=!1}),n(n.P+n.F*o,"Array",{find:function t(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),r(32)("find")},function(t,e,r){"use strict";var n=r(0),i=r(27)(6),o="findIndex",s=!0;o in[]&&Array(1)[o](function(){s=!1}),n(n.P+n.F*s,"Array",{findIndex:function t(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),r(32)(o)},function(t,e,r){r(39)("Array")},function(t,e,r){var n=r(2),i=r(79),o=r(9).f,s=r(38).f,a=r(60),u=r(53),c=n.RegExp,f=c,h=c.prototype,l=/a/g,p=/a/g,d=new c(l)!==l;if(r(8)&&(!d||r(4)(function(){return p[r(6)("match")]=!1,c(l)!=l||c(p)==p||"/a/i"!=c(l,"i")}))){c=function t(e,r){var n=this instanceof c,o=a(e),s=void 0===r;return!n&&o&&e.constructor===c&&s?e:i(d?new f(o&&!s?e.source:e,r):f((o=e instanceof c)?e.source:e,o&&s?u.call(e):r),n?this:h,c)};for(var g=function(t){t in c||o(c,t,{configurable:!0,get:function(){return f[t]},set:function(e){f[t]=e}})},v=s(f),y=0;v.length>y;)g(v[y++]);h.constructor=c,c.prototype=h,r(13)(n,"RegExp",c)}r(39)("RegExp")},function(t,e,r){"use strict";r(123);var n=r(1),i=r(53),o=r(8),s=/./.toString,a=function(t){r(13)(RegExp.prototype,"toString",t,!0)};r(4)(function(){return"/a/b"!=s.call({source:"a",flags:"b"})})?a(function t(){var e=n(this);return"/".concat(e.source,"/","flags"in e?e.flags:!o&&e instanceof RegExp?i.call(e):void 0)}):"toString"!=s.name&&a(function t(){return s.call(this)})},function(t,e,r){"use strict";var n=r(1),i=r(7),o=r(94),s=r(62);r(63)("match",1,function(t,e,r,a){return[function r(n){var i=t(this),o=void 0==n?void 0:n[e];return void 0!==o?o.call(n,i):new RegExp(n)[e](String(i))},function(t){var e=a(r,t,this);if(e.done)return e.value;var u=n(t),c=String(this);if(!u.global)return s(u,c);var f=u.unicode;u.lastIndex=0;for(var h,l=[],p=0;null!==(h=s(u,c));){var d=String(h[0]);l[p]=d,""===d&&(u.lastIndex=o(c,i(u.lastIndex),f)),p++}return 0===p?null:l}]})},function(t,e,r){"use strict";var n=r(1),i=r(10),o=r(7),s=r(22),a=r(94),u=r(62),c=Math.max,f=Math.min,h=Math.floor,l=/\$([$&`']|\d\d?|<[^>]*>)/g,p=/\$([$&`']|\d\d?)/g;r(63)("replace",2,function(t,e,r,d){return[function n(i,o){var s=t(this),a=void 0==i?void 0:i[e];return void 0!==a?a.call(i,s,o):r.call(String(s),i,o)},function(t,e){var i=d(r,t,this,e);if(i.done)return i.value;var h=n(t),l=String(this),p="function"==typeof e;p||(e=String(e));var v=h.global;if(v){var y=h.unicode;h.lastIndex=0}for(var m=[];;){var _=u(h,l);if(null===_)break;if(m.push(_),!v)break;""===String(_[0])&&(h.lastIndex=a(l,o(h.lastIndex),y))}for(var S,b="",w=0,F=0;F=w&&(b+=l.slice(w,x)+T,w=x+E.length)}return b+l.slice(w)}];function g(t,e,n,o,s,a){var u=n+t.length,c=o.length,f=p;return void 0!==s&&(s=i(s),f=l),r.call(a,f,function(r,i){var a;switch(i.charAt(0)){case"$":return"$";case"&":return t;case"`":return e.slice(0,n);case"'":return e.slice(u);case"<":a=s[i.slice(1,-1)];break;default:var f=+i;if(0===f)return r;if(f>c){var l=h(f/10);return 0===l?r:l<=c?void 0===o[l-1]?i.charAt(1):o[l-1]+i.charAt(1):r}a=o[f-1]}return void 0===a?"":a})}})},function(t,e,r){"use strict";var n=r(1),i=r(109),o=r(62);r(63)("search",1,function(t,e,r,s){return[function r(n){var i=t(this),o=void 0==n?void 0:n[e];return void 0!==o?o.call(n,i):new RegExp(n)[e](String(i))},function(t){var e=s(r,t,this);if(e.done)return e.value;var a=n(t),u=String(this),c=a.lastIndex;i(c,0)||(a.lastIndex=0);var f=o(a,u);return i(a.lastIndex,c)||(a.lastIndex=c),null===f?-1:f.index}]})},function(t,e,r){"use strict";var n=r(60),i=r(1),o=r(54),s=r(94),a=r(7),u=r(62),c=r(93),f=r(4),h=Math.min,l=[].push,p=!f(function(){RegExp(4294967295,"y")});r(63)("split",2,function(t,e,r,f){var d;return d="c"=="abbc".split(/(b)*/)[1]||4!="test".split(/(?:)/,-1).length||2!="ab".split(/(?:ab)*/).length||4!=".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length?function(t,e){var i=String(this);if(void 0===t&&0===e)return[];if(!n(t))return r.call(i,t,e);for(var o,s,a,u=[],f=(t.ignoreCase?"i":"")+(t.multiline?"m":"")+(t.unicode?"u":"")+(t.sticky?"y":""),h=0,p=void 0===e?4294967295:e>>>0,d=new RegExp(t.source,f+"g");(o=c.call(d,i))&&!((s=d.lastIndex)>h&&(u.push(i.slice(h,o.index)),o.length>1&&o.index=p));)d.lastIndex===o.index&&d.lastIndex++;return h===i.length?!a&&d.test("")||u.push(""):u.push(i.slice(h)),u.length>p?u.slice(0,p):u}:"0".split(void 0,0).length?function(t,e){return void 0===t&&0===e?[]:r.call(this,t,e)}:r,[function r(n,i){var o=t(this),s=void 0==n?void 0:n[e];return void 0!==s?s.call(n,o,i):d.call(String(o),n,i)},function(t,e){var n=f(d,t,this,e,d!==r);if(n.done)return n.value;var c=i(t),l=String(this),g=o(c,RegExp),v=c.unicode,y=(c.ignoreCase?"i":"")+(c.multiline?"m":"")+(c.unicode?"u":"")+(p?"y":"g"),m=new g(p?c:"^(?:"+c.source+")",y),_=void 0===e?4294967295:e>>>0;if(0===_)return[];if(0===l.length)return null===u(m,l)?[l]:[];for(var S=0,b=0,w=[];bo;)s(r[o++]);t._c=[],t._n=!1,e&&!t._h&&D(t)})}},D=function(t){y.call(u,function(){var e,r,n,i=t._v,o=N(t);if(o&&(e=S(function(){P?E.emit("unhandledRejection",i,t):(r=u.onunhandledrejection)?r({promise:t,reason:i}):(n=u.console)&&n.error&&n.error("Unhandled promise rejection",i)}),t._h=P||N(t)?2:1),t._a=void 0,o&&e.e)throw e.v})},N=function(t){return 1!==t._h&&0===(t._a||t._c).length},L=function(t){y.call(u,function(){var e;P?E.emit("rejectionHandled",t):(e=u.onrejectionhandled)&&e({promise:t,reason:t._v})})},M=function(t){var e=this;e._d||(e._d=!0,(e=e._w||e)._v=t,e._s=2,e._a||(e._a=e._c.slice()),O(e,!0))},j=function(t){var e,r=this;if(!r._d){r._d=!0,r=r._w||r;try{if(r===t)throw F("Promise can't be resolved itself");(e=I(t))?m(function(){var n={_w:r,_d:!1};try{e.call(t,c(j,n,1),c(M,n,1))}catch(t){M.call(n,t)}}):(r._v=t,r._s=1,O(r,!1))}catch(t){M.call({_w:r,_d:!1},t)}}};R||(k=function t(e){d(this,k,"Promise","_h"),p(e),n.call(this);try{e(c(j,this,1),c(M,this,1))}catch(t){M.call(this,t)}},(n=function t(e){this._c=[],this._a=void 0,this._s=0,this._d=!1,this._v=void 0,this._h=0,this._n=!1}).prototype=r(42)(k.prototype,{then:function t(e,r){var n=T(v(this,k));return n.ok="function"!=typeof e||e,n.fail="function"==typeof r&&r,n.domain=P?E.domain:void 0,this._c.push(n),this._a&&this._a.push(n),this._s&&O(this,!1),n.promise},catch:function(t){return this.then(void 0,t)}}),o=function(){var t=new n;this.promise=t,this.resolve=c(j,t,1),this.reject=c(M,t,1)},_.f=T=function(t){return t===k||t===s?new o(t):i(t)}),h(h.G+h.W+h.F*!R,{Promise:k}),r(45)(k,"Promise"),r(39)("Promise"),s=r(19).Promise,h(h.S+h.F*!R,"Promise",{reject:function t(e){var r=T(this);return(0,r.reject)(e),r.promise}}),h(h.S+h.F*(a||!R),"Promise",{resolve:function t(e){return w(a&&this===s?k:this,e)}}),h(h.S+h.F*!(R&&r(61)(function(t){k.all(t).catch(C)})),"Promise",{all:function t(e){var r=this,n=T(r),i=n.resolve,o=n.reject,s=S(function(){var t=[],n=0,s=1;g(e,!1,function(e){var a=n++,u=!1;t.push(void 0),s++,r.resolve(e).then(function(e){u||(u=!0,t[a]=e,--s||i(t))},o)}),--s||i(t)});return s.e&&o(s.v),n.promise},race:function t(e){var r=this,n=T(r),i=n.reject,o=S(function(){g(e,!1,function(t){r.resolve(t).then(n.resolve,i)})});return o.e&&i(o.v),n.promise}})},function(t,e,r){"use strict";var n=r(130),i=r(43);r(65)("WeakSet",function(t){return function e(){return t(this,arguments.length>0?arguments[0]:void 0)}},{add:function t(e){return n.def(i(this,"WeakSet"),e,!0)}},n,!1,!0)},function(t,e,r){"use strict";var n=r(0),i=r(66),o=r(98),s=r(1),a=r(36),u=r(7),c=r(5),f=r(2).ArrayBuffer,h=r(54),l=o.ArrayBuffer,p=o.DataView,d=i.ABV&&f.isView,g=l.prototype.slice,v=i.VIEW;n(n.G+n.W+n.F*(f!==l),{ArrayBuffer:l}),n(n.S+n.F*!i.CONSTR,"ArrayBuffer",{isView:function t(e){return d&&d(e)||c(e)&&v in e}}),n(n.P+n.U+n.F*r(4)(function(){return!new l(2).slice(1,void 0).byteLength}),"ArrayBuffer",{slice:function t(e,r){if(void 0!==g&&void 0===r)return g.call(s(this),e);for(var n=s(this).byteLength,i=a(e,n),o=a(void 0===r?n:r,n),c=new(h(this,l))(u(o-i)),f=new p(this),d=new p(c),v=0;i=e.length)return{value:void 0,done:!0}}while(!((t=e[this._i++])in this._t));return{value:t,done:!1}}),n(n.S,"Reflect",{enumerate:function t(e){return new o(e)}})},function(t,e,r){var n=r(17),i=r(18),o=r(15),s=r(0),a=r(5),u=r(1);s(s.S,"Reflect",{get:function t(e,r){var s,c,f=arguments.length<3?e:arguments[2];return u(e)===f?e[r]:(s=n.f(e,r))?o(s,"value")?s.value:void 0!==s.get?s.get.call(f):void 0:a(c=i(e))?t(c,r,f):void 0}})},function(t,e,r){var n=r(17),i=r(0),o=r(1);i(i.S,"Reflect",{getOwnPropertyDescriptor:function t(e,r){return n.f(o(e),r)}})},function(t,e,r){var n=r(0),i=r(18),o=r(1);n(n.S,"Reflect",{getPrototypeOf:function t(e){return i(o(e))}})},function(t,e,r){var n=r(0);n(n.S,"Reflect",{has:function t(e,r){return r in e}})},function(t,e,r){var n=r(0),i=r(1),o=Object.isExtensible;n(n.S,"Reflect",{isExtensible:function t(e){return i(e),!o||o(e)}})},function(t,e,r){var n=r(0);n(n.S,"Reflect",{ownKeys:r(132)})},function(t,e,r){var n=r(0),i=r(1),o=Object.preventExtensions;n(n.S,"Reflect",{preventExtensions:function t(e){i(e);try{return o&&o(e),!0}catch(t){return!1}}})},function(t,e,r){var n=r(9),i=r(17),o=r(18),s=r(15),a=r(0),u=r(33),c=r(1),f=r(5);a(a.S,"Reflect",{set:function t(e,r,a){var h,l,p=arguments.length<4?e:arguments[3],d=i.f(c(e),r);if(!d){if(f(l=o(e)))return t(l,r,a,p);d=u(0)}if(s(d,"value")){if(!1===d.writable||!f(p))return!1;if(h=i.f(p,r)){if(h.get||h.set||!1===h.writable)return!1;h.value=a,n.f(p,r,h)}else n.f(p,r,u(0,a));return!0}return void 0!==d.set&&(d.set.call(p,a),!0)}})},function(t,e,r){var n=r(0),i=r(77);i&&n(n.S,"Reflect",{setPrototypeOf:function t(e,r){i.check(e,r);try{return i.set(e,r),!0}catch(t){return!1}}})},function(t,e,r){"use strict";var n=r(0),i=r(56)(!0);n(n.P,"Array",{includes:function t(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),r(32)("includes")},function(t,e,r){"use strict";var n=r(0),i=r(133),o=r(10),s=r(7),a=r(11),u=r(90);n(n.P,"Array",{flatMap:function t(e){var r,n,c=o(this);return a(e),r=s(c.length),n=u(c,0),i(n,c,c,r,0,1,e,arguments[1]),n}}),r(32)("flatMap")},function(t,e,r){"use strict";var n=r(0),i=r(133),o=r(10),s=r(7),a=r(22),u=r(90);n(n.P,"Array",{flatten:function t(){var e=arguments[0],r=o(this),n=s(r.length),c=u(r,0);return i(c,r,r,n,0,void 0===e?1:a(e)),c}}),r(32)("flatten")},function(t,e,r){"use strict";var n=r(0),i=r(59)(!0);n(n.P,"String",{at:function t(e){return i(this,e)}})},function(t,e,r){"use strict";var n=r(0),i=r(134),o=r(64);n(n.P+n.F*/Version\/10\.\d+(\.\d+)? Safari\//.test(o),"String",{padStart:function t(e){return i(this,e,arguments.length>1?arguments[1]:void 0,!0)}})},function(t,e,r){"use strict";var n=r(0),i=r(134),o=r(64);n(n.P+n.F*/Version\/10\.\d+(\.\d+)? Safari\//.test(o),"String",{padEnd:function t(e){return i(this,e,arguments.length>1?arguments[1]:void 0,!1)}})},function(t,e,r){"use strict";r(47)("trimLeft",function(t){return function e(){return t(this,1)}},"trimStart")},function(t,e,r){"use strict";r(47)("trimRight",function(t){return function e(){return t(this,2)}},"trimEnd")},function(t,e,r){"use strict";var n=r(0),i=r(25),o=r(7),s=r(60),a=r(53),u=RegExp.prototype,c=function(t,e){this._r=t,this._s=e};r(84)(c,"RegExp String",function t(){var e=this._r.exec(this._s);return{value:e,done:null===e}}),n(n.P,"String",{matchAll:function t(e){if(i(this),!s(e))throw TypeError(e+" is not a regexp!");var r=String(this),n="flags"in u?String(e.flags):a.call(e),f=new RegExp(e.source,~n.indexOf("g")?n:"g"+n);return f.lastIndex=o(e.lastIndex),new c(f,r)}})},function(t,e,r){r(73)("asyncIterator")},function(t,e,r){r(73)("observable")},function(t,e,r){var n=r(0),i=r(132),o=r(16),s=r(17),a=r(88);n(n.S,"Object",{getOwnPropertyDescriptors:function t(e){for(var r,n,u=o(e),c=s.f,f=i(u),h={},l=0;f.length>l;)void 0!==(n=c(u,r=f[l++]))&&a(h,r,n);return h}})},function(t,e,r){var n=r(0),i=r(135)(!1);n(n.S,"Object",{values:function t(e){return i(e)}})},function(t,e,r){var n=r(0),i=r(135)(!0);n(n.S,"Object",{entries:function t(e){return i(e)}})},function(t,e,r){"use strict";var n=r(0),i=r(10),o=r(11),s=r(9);r(8)&&n(n.P+r(67),"Object",{__defineGetter__:function t(e,r){s.f(i(this),e,{get:o(r),enumerable:!0,configurable:!0})}})},function(t,e,r){"use strict";var n=r(0),i=r(10),o=r(11),s=r(9);r(8)&&n(n.P+r(67),"Object",{__defineSetter__:function t(e,r){s.f(i(this),e,{set:o(r),enumerable:!0,configurable:!0})}})},function(t,e,r){"use strict";var n=r(0),i=r(10),o=r(24),s=r(18),a=r(17).f;r(8)&&n(n.P+r(67),"Object",{__lookupGetter__:function t(e){var r,n=i(this),u=o(e,!0);do{if(r=a(n,u))return r.get}while(n=s(n))}})},function(t,e,r){"use strict";var n=r(0),i=r(10),o=r(24),s=r(18),a=r(17).f;r(8)&&n(n.P+r(67),"Object",{__lookupSetter__:function t(e){var r,n=i(this),u=o(e,!0);do{if(r=a(n,u))return r.set}while(n=s(n))}})},function(t,e,r){var n=r(0);n(n.P+n.R,"Map",{toJSON:r(136)("Map")})},function(t,e,r){var n=r(0);n(n.P+n.R,"Set",{toJSON:r(136)("Set")})},function(t,e,r){r(68)("Map")},function(t,e,r){r(68)("Set")},function(t,e,r){r(68)("WeakMap")},function(t,e,r){r(68)("WeakSet")},function(t,e,r){r(69)("Map")},function(t,e,r){r(69)("Set")},function(t,e,r){r(69)("WeakMap")},function(t,e,r){r(69)("WeakSet")},function(t,e,r){var n=r(0);n(n.G,{global:r(2)})},function(t,e,r){var n=r(0);n(n.S,"System",{global:r(2)})},function(t,e,r){var n=r(0),i=r(21);n(n.S,"Error",{isError:function t(e){return"Error"===i(e)}})},function(t,e,r){var n=r(0);n(n.S,"Math",{clamp:function t(e,r,n){return Math.min(n,Math.max(r,e))}})},function(t,e,r){var n=r(0);n(n.S,"Math",{DEG_PER_RAD:Math.PI/180})},function(t,e,r){var n=r(0),i=180/Math.PI;n(n.S,"Math",{degrees:function t(e){return e*i}})},function(t,e,r){var n=r(0),i=r(138),o=r(117);n(n.S,"Math",{fscale:function t(e,r,n,s,a){return o(i(e,r,n,s,a))}})},function(t,e,r){var n=r(0);n(n.S,"Math",{iaddh:function t(e,r,n,i){var o=e>>>0,s=n>>>0;return(r>>>0)+(i>>>0)+((o&s|(o|s)&~(o+s>>>0))>>>31)|0}})},function(t,e,r){var n=r(0);n(n.S,"Math",{isubh:function t(e,r,n,i){var o=e>>>0,s=n>>>0;return(r>>>0)-(i>>>0)-((~o&s|~(o^s)&o-s>>>0)>>>31)|0}})},function(t,e,r){var n=r(0);n(n.S,"Math",{imulh:function t(e,r){var n=+e,i=+r,o=65535&n,s=65535&i,a=n>>16,u=i>>16,c=(a*s>>>0)+(o*s>>>16);return a*u+(c>>16)+((o*u>>>0)+(65535&c)>>16)}})},function(t,e,r){var n=r(0);n(n.S,"Math",{RAD_PER_DEG:180/Math.PI})},function(t,e,r){var n=r(0),i=Math.PI/180;n(n.S,"Math",{radians:function t(e){return e*i}})},function(t,e,r){var n=r(0);n(n.S,"Math",{scale:r(138)})},function(t,e,r){var n=r(0);n(n.S,"Math",{umulh:function t(e,r){var n=+e,i=+r,o=65535&n,s=65535&i,a=n>>>16,u=i>>>16,c=(a*s>>>0)+(o*s>>>16);return a*u+(c>>>16)+((o*u>>>0)+(65535&c)>>>16)}})},function(t,e,r){var n=r(0);n(n.S,"Math",{signbit:function t(e){return(e=+e)!=e?e:0==e?1/e==1/0:e>0}})},function(t,e,r){"use strict";var n=r(0),i=r(19),o=r(2),s=r(54),a=r(125);n(n.P+n.R,"Promise",{finally:function(t){var e=s(this,i.Promise||o.Promise),r="function"==typeof t;return this.then(r?function(r){return a(e,t()).then(function(){return r})}:t,r?function(r){return a(e,t()).then(function(){throw r})}:t)}})},function(t,e,r){"use strict";var n=r(0),i=r(97),o=r(124);n(n.S,"Promise",{try:function(t){var e=i.f(this),r=o(t);return(r.e?e.reject:e.resolve)(r.v),e.promise}})},function(t,e,r){var n=r(29),i=r(1),o=n.key,s=n.set;n.exp({defineMetadata:function t(e,r,n,a){s(e,r,i(n),o(a))}})},function(t,e,r){var n=r(29),i=r(1),o=n.key,s=n.map,a=n.store;n.exp({deleteMetadata:function t(e,r){var n=arguments.length<3?void 0:o(arguments[2]),u=s(i(r),n,!1);if(void 0===u||!u.delete(e))return!1;if(u.size)return!0;var c=a.get(r);return c.delete(n),!!c.size||a.delete(r)}})},function(t,e,r){var n=r(29),i=r(1),o=r(18),s=n.has,a=n.get,u=n.key,c=function(t,e,r){if(s(t,e,r))return a(t,e,r);var n=o(e);return null!==n?c(t,n,r):void 0};n.exp({getMetadata:function t(e,r){return c(e,i(r),arguments.length<3?void 0:u(arguments[2]))}})},function(t,e,r){var n=r(128),i=r(137),o=r(29),s=r(1),a=r(18),u=o.keys,c=o.key,f=function(t,e){var r=u(t,e),o=a(t);if(null===o)return r;var s=f(o,e);return s.length?r.length?i(new n(r.concat(s))):s:r};o.exp({getMetadataKeys:function t(e){return f(s(e),arguments.length<2?void 0:c(arguments[1]))}})},function(t,e,r){var n=r(29),i=r(1),o=n.get,s=n.key;n.exp({getOwnMetadata:function t(e,r){return o(e,i(r),arguments.length<3?void 0:s(arguments[2]))}})},function(t,e,r){var n=r(29),i=r(1),o=n.keys,s=n.key;n.exp({getOwnMetadataKeys:function t(e){return o(i(e),arguments.length<2?void 0:s(arguments[1]))}})},function(t,e,r){var n=r(29),i=r(1),o=r(18),s=n.has,a=n.key,u=function(t,e,r){if(s(t,e,r))return!0;var n=o(e);return null!==n&&u(t,n,r)};n.exp({hasMetadata:function t(e,r){return u(e,i(r),arguments.length<3?void 0:a(arguments[2]))}})},function(t,e,r){var n=r(29),i=r(1),o=n.has,s=n.key;n.exp({hasOwnMetadata:function t(e,r){return o(e,i(r),arguments.length<3?void 0:s(arguments[2]))}})},function(t,e,r){var n=r(29),i=r(1),o=r(11),s=n.key,a=n.set;n.exp({metadata:function t(e,r){return function t(n,u){a(e,r,(void 0!==u?i:o)(n),s(u))}}})},function(t,e,r){var n=r(0),i=r(96)(),o=r(2).process,s="process"==r(21)(o);n(n.G,{asap:function t(e){var r=s&&o.domain;i(r?r.bind(e):e)}})},function(t,e,r){"use strict";var n=r(0),i=r(2),o=r(19),s=r(96)(),a=r(6)("observable"),u=r(11),c=r(1),f=r(40),h=r(42),l=r(12),p=r(41),d=p.RETURN,g=function(t){return null==t?void 0:u(t)},v=function(t){var e=t._c;e&&(t._c=void 0,e())},y=function(t){return void 0===t._o},m=function(t){y(t)||(t._o=void 0,v(t))},_=function(t,e){c(t),this._c=void 0,this._o=t,t=new S(this);try{var r=e(t),n=r;null!=r&&("function"==typeof r.unsubscribe?r=function(){n.unsubscribe()}:u(r),this._c=r)}catch(e){return void t.error(e)}y(this)&&v(this)};_.prototype=h({},{unsubscribe:function t(){m(this)}});var S=function(t){this._s=t};S.prototype=h({},{next:function t(e){var r=this._s;if(!y(r)){var n=r._o;try{var i=g(n.next);if(i)return i.call(n,e)}catch(t){try{m(r)}finally{throw t}}}},error:function t(e){var r=this._s;if(y(r))throw e;var n=r._o;r._o=void 0;try{var i=g(n.error);if(!i)throw e;e=i.call(n,e)}catch(t){try{v(r)}finally{throw t}}return v(r),e},complete:function t(e){var r=this._s;if(!y(r)){var n=r._o;r._o=void 0;try{var i=g(n.complete);e=i?i.call(n,e):void 0}catch(t){try{v(r)}finally{throw t}}return v(r),e}}});var b=function t(e){f(this,b,"Observable","_f")._f=u(e)};h(b.prototype,{subscribe:function t(e){return new _(e,this._f)},forEach:function t(e){var r=this;return new(o.Promise||i.Promise)(function(t,n){u(e);var i=r.subscribe({next:function(t){try{return e(t)}catch(t){n(t),i.unsubscribe()}},error:n,complete:t})})}}),h(b,{from:function t(e){var r="function"==typeof this?this:b,n=g(c(e)[a]);if(n){var i=c(n.call(e));return i.constructor===r?i:new r(function(t){return i.subscribe(t)})}return new r(function(t){var r=!1;return s(function(){if(!r){try{if(p(e,!1,function(e){if(t.next(e),r)return d})===d)return}catch(e){if(r)throw e;return void t.error(e)}t.complete()}}),function(){r=!0}})},of:function t(){for(var e=0,r=arguments.length,n=new Array(r);e2,i=!!n&&s.call(arguments,2);return t(n?function(){("function"==typeof e?e:Function(e)).apply(this,i)}:e,r)}};i(i.G+i.B+i.F*a,{setTimeout:u(n.setTimeout),setInterval:u(n.setInterval)})},function(t,e,r){var n=r(0),i=r(95);n(n.G+n.B,{setImmediate:i.set,clearImmediate:i.clear})},function(t,e,r){for(var n=r(92),i=r(35),o=r(13),s=r(2),a=r(12),u=r(48),c=r(6),f=c("iterator"),h=c("toStringTag"),l=u.Array,p={CSSRuleList:!0,CSSStyleDeclaration:!1,CSSValueList:!1,ClientRectList:!1,DOMRectList:!1,DOMStringList:!1,DOMTokenList:!0,DataTransferItemList:!1,FileList:!1,HTMLAllCollection:!1,HTMLCollection:!1,HTMLFormElement:!1,HTMLSelectElement:!1,MediaList:!0,MimeTypeArray:!1,NamedNodeMap:!1,NodeList:!0,PaintRequestList:!1,Plugin:!1,PluginArray:!1,SVGLengthList:!1,SVGNumberList:!1,SVGPathSegList:!1,SVGPointList:!1,SVGStringList:!1,SVGTransformList:!1,SourceBufferList:!1,StyleSheetList:!0,TextTrackCueList:!1,TextTrackList:!1,TouchList:!1},d=i(p),g=0;g=0;--o){var s=this.tryEntries[o],a=s.completion;if("root"===s.tryLoc)return n("end");if(s.tryLoc<=this.prev){var u=i.call(s,"catchLoc"),c=i.call(s,"finallyLoc");if(u&&c){if(this.prev=0;--r){var n=this.tryEntries[r];if(n.tryLoc<=this.prev&&i.call(n,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),C(r),g}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var i=n.arg;C(r)}return i}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,n){return this.delegate={iterator:R(t),resultName:e,nextLoc:n},"next"===this.method&&(this.arg=r),g}}}function S(t,e,r,n){var i=e&&e.prototype instanceof w?e:w,o=Object.create(i.prototype),s=new T(n||[]);return o._invoke=function a(t,e,r){var n=h;return function i(o,s){if(n===p)throw new Error("Generator is already running");if(n===d){if("throw"===o)throw s;return I()}for(r.method=o,r.arg=s;;){var a=r.delegate;if(a){var u=k(a,r);if(u){if(u===g)continue;return u}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if(n===h)throw n=d,r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);n=p;var c=b(t,e,r);if("normal"===c.type){if(n=r.done?d:l,c.arg===g)continue;return{value:c.arg,done:r.done}}"throw"===c.type&&(n=d,r.method="throw",r.arg=c.arg)}}}(t,r,s),o}function b(t,e,r){try{return{type:"normal",arg:t.call(e,r)}}catch(t){return{type:"throw",arg:t}}}function w(){}function F(){}function E(){}function x(t){["next","throw","return"].forEach(function(e){t[e]=function(t){return this._invoke(e,t)}})}function A(t){function r(e,n,o,s){var a=b(t[e],t,n);if("throw"!==a.type){var u=a.arg,c=u.value;return c&&"object"==typeof c&&i.call(c,"__await")?Promise.resolve(c.__await).then(function(t){r("next",t,o,s)},function(t){r("throw",t,o,s)}):Promise.resolve(c).then(function(t){u.value=t,o(u)},s)}s(a.arg)}var n;"object"==typeof e.process&&e.process.domain&&(r=e.process.domain.bind(r)),this._invoke=function o(t,e){function i(){return new Promise(function(n,i){r(t,e,n,i)})}return n=n?n.then(i,i):i()}}function k(t,e){var n=t.iterator[e.method];if(n===r){if(e.delegate=null,"throw"===e.method){if(t.iterator.return&&(e.method="return",e.arg=r,k(t,e),"throw"===e.method))return g;e.method="throw",e.arg=new TypeError("The iterator does not provide a 'throw' method")}return g}var i=b(n,t.iterator,e.arg);if("throw"===i.type)return e.method="throw",e.arg=i.arg,e.delegate=null,g;var o=i.arg;return o?o.done?(e[t.resultName]=o.value,e.next=t.nextLoc,"return"!==e.method&&(e.method="next",e.arg=r),e.delegate=null,g):o:(e.method="throw",e.arg=new TypeError("iterator result is not an object"),e.delegate=null,g)}function P(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function C(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function T(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(P,this),this.reset(!0)}function R(t){if(t){var e=t[s];if(e)return e.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var n=-1,o=function e(){for(;++n1&&void 0!==arguments[1]?arguments[1]:o.MetadataService,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:s.UserInfoService,u=arguments.length>3&&void 0!==arguments[3]?arguments[3]:c.JoseUtil,f=arguments.length>4&&void 0!==arguments[4]?arguments[4]:a.TokenClient;if(function h(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!e)throw i.Log.error("ResponseValidator.ctor: No settings passed to ResponseValidator"),new Error("settings");this._settings=e,this._metadataService=new r(this._settings),this._userInfoService=new n(this._settings),this._joseUtil=u,this._tokenClient=new f(this._settings)}return t.prototype.validateSigninResponse=function t(e,r){var n=this;return i.Log.debug("ResponseValidator.validateSigninResponse"),this._processSigninParams(e,r).then(function(t){return i.Log.debug("ResponseValidator.validateSigninResponse: state processed"),n._validateTokens(e,t).then(function(t){return i.Log.debug("ResponseValidator.validateSigninResponse: tokens validated"),n._processClaims(e,t).then(function(t){return i.Log.debug("ResponseValidator.validateSigninResponse: claims processed"),t})})})},t.prototype.validateSignoutResponse=function t(e,r){return e.id!==r.state?(i.Log.error("ResponseValidator.validateSignoutResponse: State does not match"),Promise.reject(new Error("State does not match"))):(i.Log.debug("ResponseValidator.validateSignoutResponse: state validated"),r.state=e.data,r.error?(i.Log.warn("ResponseValidator.validateSignoutResponse: Response was error",r.error),Promise.reject(new u.ErrorResponse(r))):Promise.resolve(r))},t.prototype._processSigninParams=function t(e,r){if(e.id!==r.state)return i.Log.error("ResponseValidator._processSigninParams: State does not match"),Promise.reject(new Error("State does not match"));if(!e.client_id)return i.Log.error("ResponseValidator._processSigninParams: No client_id on state"),Promise.reject(new Error("No client_id on state"));if(!e.authority)return i.Log.error("ResponseValidator._processSigninParams: No authority on state"),Promise.reject(new Error("No authority on state"));if(this._settings.authority){if(this._settings.authority&&this._settings.authority!==e.authority)return i.Log.error("ResponseValidator._processSigninParams: authority mismatch on settings vs. signin state"),Promise.reject(new Error("authority mismatch on settings vs. signin state"))}else this._settings.authority=e.authority;if(this._settings.client_id){if(this._settings.client_id&&this._settings.client_id!==e.client_id)return i.Log.error("ResponseValidator._processSigninParams: client_id mismatch on settings vs. signin state"),Promise.reject(new Error("client_id mismatch on settings vs. signin state"))}else this._settings.client_id=e.client_id;return i.Log.debug("ResponseValidator._processSigninParams: state validated"),r.state=e.data,r.error?(i.Log.warn("ResponseValidator._processSigninParams: Response was error",r.error),Promise.reject(new u.ErrorResponse(r))):e.nonce&&!r.id_token?(i.Log.error("ResponseValidator._processSigninParams: Expecting id_token in response"),Promise.reject(new Error("No id_token in response"))):!e.nonce&&r.id_token?(i.Log.error("ResponseValidator._processSigninParams: Not expecting id_token in response"),Promise.reject(new Error("Unexpected id_token in response"))):e.code_verifier&&!r.code?(i.Log.error("ResponseValidator._processSigninParams: Expecting code in response"),Promise.reject(new Error("No code in response"))):!e.code_verifier&&r.code?(i.Log.error("ResponseValidator._processSigninParams: Not expecting code in response"),Promise.reject(new Error("Unexpected code in response"))):(r.scope||(r.scope=e.scope),Promise.resolve(r))},t.prototype._processClaims=function t(e,r){var n=this;if(r.isOpenIdConnect){if(i.Log.debug("ResponseValidator._processClaims: response is OIDC, processing claims"),r.profile=this._filterProtocolClaims(r.profile),!0!==e.skipUserInfo&&this._settings.loadUserInfo&&r.access_token)return i.Log.debug("ResponseValidator._processClaims: loading user info"),this._userInfoService.getClaims(r.access_token).then(function(t){return i.Log.debug("ResponseValidator._processClaims: user info claims received from user info endpoint"),t.sub!==r.profile.sub?(i.Log.error("ResponseValidator._processClaims: sub from user info endpoint does not match sub in access_token"),Promise.reject(new Error("sub from user info endpoint does not match sub in access_token"))):(r.profile=n._mergeClaims(r.profile,t),i.Log.debug("ResponseValidator._processClaims: user info claims received, updated profile:",r.profile),r)});i.Log.debug("ResponseValidator._processClaims: not loading user info")}else i.Log.debug("ResponseValidator._processClaims: response is not OIDC, not processing claims");return Promise.resolve(r)},t.prototype._mergeClaims=function t(e,r){var i=Object.assign({},e);for(var o in r){var s=r[o];Array.isArray(s)||(s=[s]);for(var a=0;a1)return i.Log.error("ResponseValidator._validateIdToken: No kid found in id_token and more than one key found in metadata"),Promise.reject(new Error("No kid found in id_token and more than one key found in metadata"));u=a[0]}if(!u)return i.Log.error("ResponseValidator._validateIdToken: No key matching kid or alg found in signing keys"),Promise.reject(new Error("No key matching kid or alg found in signing keys"));var c=e.client_id,f=n._settings.clockSkew;return i.Log.debug("ResponseValidator._validateIdToken: Validaing JWT; using clock skew (in seconds) of: ",f),n._joseUtil.validateJwt(r.id_token,u,t,c,f).then(function(){return i.Log.debug("ResponseValidator._validateIdToken: JWT validation successful"),o.payload.sub?(r.profile=o.payload,r):(i.Log.error("ResponseValidator._validateIdToken: No sub present in id_token"),Promise.reject(new Error("No sub present in id_token")))})})})},t.prototype._filterByAlg=function t(e,r){var n=null;if(r.startsWith("RS"))n="RSA";else if(r.startsWith("PS"))n="PS";else{if(!r.startsWith("ES"))return i.Log.debug("ResponseValidator._filterByAlg: alg not supported: ",r),[];n="EC"}return i.Log.debug("ResponseValidator._filterByAlg: Looking for keys that match kty: ",n),e=e.filter(function(t){return t.kty===n}),i.Log.debug("ResponseValidator._filterByAlg: Number of keys that match kty: ",n,e.length),e},t.prototype._validateAccessToken=function t(e){if(!e.profile)return i.Log.error("ResponseValidator._validateAccessToken: No profile loaded from id_token"),Promise.reject(new Error("No profile loaded from id_token"));if(!e.profile.at_hash)return i.Log.error("ResponseValidator._validateAccessToken: No at_hash in id_token"),Promise.reject(new Error("No at_hash in id_token"));if(!e.id_token)return i.Log.error("ResponseValidator._validateAccessToken: No id_token"),Promise.reject(new Error("No id_token"));var r=this._joseUtil.parseJwt(e.id_token);if(!r||!r.header)return i.Log.error("ResponseValidator._validateAccessToken: Failed to parse id_token",r),Promise.reject(new Error("Failed to parse id_token"));var n=r.header.alg;if(!n||5!==n.length)return i.Log.error("ResponseValidator._validateAccessToken: Unsupported alg:",n),Promise.reject(new Error("Unsupported alg: "+n));var o=n.substr(2,3);if(!o)return i.Log.error("ResponseValidator._validateAccessToken: Unsupported alg:",n,o),Promise.reject(new Error("Unsupported alg: "+n));if(256!==(o=parseInt(o))&&384!==o&&512!==o)return i.Log.error("ResponseValidator._validateAccessToken: Unsupported alg:",n,o),Promise.reject(new Error("Unsupported alg: "+n));var s="sha"+o,a=this._joseUtil.hashString(e.access_token,s);if(!a)return i.Log.error("ResponseValidator._validateAccessToken: access_token hash failed:",s),Promise.reject(new Error("Failed to validate at_hash"));var u=a.substr(0,a.length/2),c=this._joseUtil.hexToBase64Url(u);return c!==e.profile.at_hash?(i.Log.error("ResponseValidator._validateAccessToken: Failed to validate at_hash",c,e.profile.at_hash),Promise.reject(new Error("Failed to validate at_hash"))):(i.Log.debug("ResponseValidator._validateAccessToken: success"),Promise.resolve(e))},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.UserInfoService=void 0;var n=r(101),i=r(49),o=r(3),s=r(70);e.UserInfoService=function(){function t(e){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:n.JsonService,a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:i.MetadataService,u=arguments.length>3&&void 0!==arguments[3]?arguments[3]:s.JoseUtil;if(function c(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!e)throw o.Log.error("UserInfoService.ctor: No settings passed"),new Error("settings");this._settings=e,this._jsonService=new r(void 0,void 0,this._getClaimsFromJwt.bind(this)),this._metadataService=new a(this._settings),this._joseUtil=u}return t.prototype.getClaims=function t(e){var r=this;return e?this._metadataService.getUserInfoEndpoint().then(function(t){return o.Log.debug("UserInfoService.getClaims: received userinfo url",t),r._jsonService.getJson(t,e).then(function(t){return o.Log.debug("UserInfoService.getClaims: claims received",t),t})}):(o.Log.error("UserInfoService.getClaims: No token passed"),Promise.reject(new Error("A token is required")))},t.prototype._getClaimsFromJwt=function t(e){var r=this;try{var n=this._joseUtil.parseJwt(e.responseText);if(!n||!n.header||!n.payload)return o.Log.error("UserInfoService._getClaimsFromJwt: Failed to parse JWT",n),Promise.reject(new Error("Failed to parse id_token"));var i=n.header.kid,s=void 0;switch(this._settings.userInfoJwtIssuer){case"OP":s=this._metadataService.getIssuer();break;case"ANY":s=Promise.resolve(n.payload.iss);break;default:s=Promise.resolve(this._settings.userInfoJwtIssuer)}return s.then(function(t){return o.Log.debug("UserInfoService._getClaimsFromJwt: Received issuer:"+t),r._metadataService.getSigningKeys().then(function(s){if(!s)return o.Log.error("UserInfoService._getClaimsFromJwt: No signing keys from metadata"),Promise.reject(new Error("No signing keys from metadata"));o.Log.debug("UserInfoService._getClaimsFromJwt: Received signing keys");var a=void 0;if(i)a=s.filter(function(t){return t.kid===i})[0];else{if((s=r._filterByAlg(s,n.header.alg)).length>1)return o.Log.error("UserInfoService._getClaimsFromJwt: No kid found in id_token and more than one key found in metadata"),Promise.reject(new Error("No kid found in id_token and more than one key found in metadata"));a=s[0]}if(!a)return o.Log.error("UserInfoService._getClaimsFromJwt: No key matching kid or alg found in signing keys"),Promise.reject(new Error("No key matching kid or alg found in signing keys"));var u=r._settings.client_id,c=r._settings.clockSkew;return o.Log.debug("UserInfoService._getClaimsFromJwt: Validaing JWT; using clock skew (in seconds) of: ",c),r._joseUtil.validateJwt(e.responseText,a,t,u,c,void 0,!0).then(function(){return o.Log.debug("UserInfoService._getClaimsFromJwt: JWT validation successful"),n.payload})})})}catch(t){return o.Log.error("UserInfoService._getClaimsFromJwt: Error parsing JWT response",t.message),void reject(t)}},t.prototype._filterByAlg=function t(e,r){var n=null;if(r.startsWith("RS"))n="RSA";else if(r.startsWith("PS"))n="PS";else{if(!r.startsWith("ES"))return o.Log.debug("UserInfoService._filterByAlg: alg not supported: ",r),[];n="EC"}return o.Log.debug("UserInfoService._filterByAlg: Looking for keys that match kty: ",n),e=e.filter(function(t){return t.kty===n}),o.Log.debug("UserInfoService._filterByAlg: Number of keys that match kty: ",n,e.length),e},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AllowedSigningAlgs=e.b64tohex=e.hextob64u=e.crypto=e.X509=e.KeyUtil=e.jws=void 0;var n=r(359);e.jws=n.jws,e.KeyUtil=n.KEYUTIL,e.X509=n.X509,e.crypto=n.crypto,e.hextob64u=n.hextob64u,e.b64tohex=n.b64tohex,e.AllowedSigningAlgs=["RS256","RS384","RS512","PS256","PS384","PS512","ES256","ES384","ES512"]},function(t,e,r){"use strict";(function(t){Object.defineProperty(e,"__esModule",{value:!0});var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},n={userAgent:!1},i={}; +/*! +Copyright (c) 2011, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.com/yui/license.html +version: 2.9.0 +*/ +if(void 0===o)var o={};o.lang={extend:function t(e,r,i){if(!r||!e)throw new Error("YAHOO.lang.extend failed, please check that all dependencies are included.");var o=function t(){};if(o.prototype=r.prototype,e.prototype=new o,e.prototype.constructor=e,e.superclass=r.prototype,r.prototype.constructor==Object.prototype.constructor&&(r.prototype.constructor=r),i){var s;for(s in i)e.prototype[s]=i[s];var a=function t(){},u=["toString","valueOf"];try{/MSIE/.test(n.userAgent)&&(a=function t(e,r){for(s=0;s>>2]>>>24-s%4*8&255;r[i+s>>>2]|=a<<24-(i+s)%4*8}else for(s=0;s>>2]=n[s>>>2];return this.sigBytes+=o,this},clamp:function t(){var e=this.words,r=this.sigBytes;e[r>>>2]&=4294967295<<32-r%4*8,e.length=s.ceil(r/4)},clone:function t(){var e=c.clone.call(this);return e.words=this.words.slice(0),e},random:function t(e){for(var r=[],n=0;n>>2]>>>24-o%4*8&255;i.push((s>>>4).toString(16)),i.push((15&s).toString(16))}return i.join("")},parse:function t(e){for(var r=e.length,n=[],i=0;i>>3]|=parseInt(e.substr(i,2),16)<<24-i%8*4;return new f.init(n,r/2)}},p=h.Latin1={stringify:function t(e){for(var r=e.words,n=e.sigBytes,i=[],o=0;o>>2]>>>24-o%4*8&255;i.push(String.fromCharCode(s))}return i.join("")},parse:function t(e){for(var r=e.length,n=[],i=0;i>>2]|=(255&e.charCodeAt(i))<<24-i%4*8;return new f.init(n,r)}},d=h.Utf8={stringify:function t(e){try{return decodeURIComponent(escape(p.stringify(e)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function t(e){return p.parse(unescape(encodeURIComponent(e)))}},g=u.BufferedBlockAlgorithm=c.extend({reset:function t(){this._data=new f.init,this._nDataBytes=0},_append:function t(e){"string"==typeof e&&(e=d.parse(e)),this._data.concat(e),this._nDataBytes+=e.sigBytes},_process:function t(e){var r=this._data,n=r.words,i=r.sigBytes,o=this.blockSize,a=i/(4*o),u=(a=e?s.ceil(a):s.max((0|a)-this._minBufferSize,0))*o,c=s.min(4*u,i);if(u){for(var h=0;h>>2]>>>24-o%4*8&255)<<16|(r[o+1>>>2]>>>24-(o+1)%4*8&255)<<8|r[o+2>>>2]>>>24-(o+2)%4*8&255,a=0;4>a&&o+.75*a>>6*(3-a)&63));if(r=i.charAt(64))for(;e.length%4;)e.push(r);return e.join("")},parse:function t(r){var n=r.length,i=this._map;(o=i.charAt(64))&&(-1!=(o=r.indexOf(o))&&(n=o));for(var o=[],s=0,a=0;a>>6-a%4*2;o[s>>>2]|=(u|c)<<24-s%4*8,s++}return e.create(o,s)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(),function(t){for(var e=y,r=(i=e.lib).WordArray,n=i.Hasher,i=e.algo,o=[],s=[],a=function t(e){return 4294967296*(e-(0|e))|0},u=2,c=0;64>c;){var f;t:{f=u;for(var h=t.sqrt(f),l=2;l<=h;l++)if(!(f%l)){f=!1;break t}f=!0}f&&(8>c&&(o[c]=a(t.pow(u,.5))),s[c]=a(t.pow(u,1/3)),c++),u++}var p=[];i=i.SHA256=n.extend({_doReset:function t(){this._hash=new r.init(o.slice(0))},_doProcessBlock:function t(e,r){for(var n=this._hash.words,i=n[0],o=n[1],a=n[2],u=n[3],c=n[4],f=n[5],h=n[6],l=n[7],d=0;64>d;d++){if(16>d)p[d]=0|e[r+d];else{var g=p[d-15],v=p[d-2];p[d]=((g<<25|g>>>7)^(g<<14|g>>>18)^g>>>3)+p[d-7]+((v<<15|v>>>17)^(v<<13|v>>>19)^v>>>10)+p[d-16]}g=l+((c<<26|c>>>6)^(c<<21|c>>>11)^(c<<7|c>>>25))+(c&f^~c&h)+s[d]+p[d],v=((i<<30|i>>>2)^(i<<19|i>>>13)^(i<<10|i>>>22))+(i&o^i&a^o&a),l=h,h=f,f=c,c=u+g|0,u=a,a=o,o=i,i=g+v|0}n[0]=n[0]+i|0,n[1]=n[1]+o|0,n[2]=n[2]+a|0,n[3]=n[3]+u|0,n[4]=n[4]+c|0,n[5]=n[5]+f|0,n[6]=n[6]+h|0,n[7]=n[7]+l|0},_doFinalize:function e(){var r=this._data,n=r.words,i=8*this._nDataBytes,o=8*r.sigBytes;return n[o>>>5]|=128<<24-o%32,n[14+(o+64>>>9<<4)]=t.floor(i/4294967296),n[15+(o+64>>>9<<4)]=i,r.sigBytes=4*n.length,this._process(),this._hash},clone:function t(){var e=n.clone.call(this);return e._hash=this._hash.clone(),e}});e.SHA256=n._createHelper(i),e.HmacSHA256=n._createHmacHelper(i)}(Math),function(){function t(){return n.create.apply(n,arguments)}for(var e=y,r=e.lib.Hasher,n=(o=e.x64).Word,i=o.WordArray,o=e.algo,s=[t(1116352408,3609767458),t(1899447441,602891725),t(3049323471,3964484399),t(3921009573,2173295548),t(961987163,4081628472),t(1508970993,3053834265),t(2453635748,2937671579),t(2870763221,3664609560),t(3624381080,2734883394),t(310598401,1164996542),t(607225278,1323610764),t(1426881987,3590304994),t(1925078388,4068182383),t(2162078206,991336113),t(2614888103,633803317),t(3248222580,3479774868),t(3835390401,2666613458),t(4022224774,944711139),t(264347078,2341262773),t(604807628,2007800933),t(770255983,1495990901),t(1249150122,1856431235),t(1555081692,3175218132),t(1996064986,2198950837),t(2554220882,3999719339),t(2821834349,766784016),t(2952996808,2566594879),t(3210313671,3203337956),t(3336571891,1034457026),t(3584528711,2466948901),t(113926993,3758326383),t(338241895,168717936),t(666307205,1188179964),t(773529912,1546045734),t(1294757372,1522805485),t(1396182291,2643833823),t(1695183700,2343527390),t(1986661051,1014477480),t(2177026350,1206759142),t(2456956037,344077627),t(2730485921,1290863460),t(2820302411,3158454273),t(3259730800,3505952657),t(3345764771,106217008),t(3516065817,3606008344),t(3600352804,1432725776),t(4094571909,1467031594),t(275423344,851169720),t(430227734,3100823752),t(506948616,1363258195),t(659060556,3750685593),t(883997877,3785050280),t(958139571,3318307427),t(1322822218,3812723403),t(1537002063,2003034995),t(1747873779,3602036899),t(1955562222,1575990012),t(2024104815,1125592928),t(2227730452,2716904306),t(2361852424,442776044),t(2428436474,593698344),t(2756734187,3733110249),t(3204031479,2999351573),t(3329325298,3815920427),t(3391569614,3928383900),t(3515267271,566280711),t(3940187606,3454069534),t(4118630271,4000239992),t(116418474,1914138554),t(174292421,2731055270),t(289380356,3203993006),t(460393269,320620315),t(685471733,587496836),t(852142971,1086792851),t(1017036298,365543100),t(1126000580,2618297676),t(1288033470,3409855158),t(1501505948,4234509866),t(1607167915,987167468),t(1816402316,1246189591)],a=[],u=0;80>u;u++)a[u]=t();o=o.SHA512=r.extend({_doReset:function t(){this._hash=new i.init([new n.init(1779033703,4089235720),new n.init(3144134277,2227873595),new n.init(1013904242,4271175723),new n.init(2773480762,1595750129),new n.init(1359893119,2917565137),new n.init(2600822924,725511199),new n.init(528734635,4215389547),new n.init(1541459225,327033209)])},_doProcessBlock:function t(e,r){for(var n=(l=this._hash.words)[0],i=l[1],o=l[2],u=l[3],c=l[4],f=l[5],h=l[6],l=l[7],p=n.high,d=n.low,g=i.high,v=i.low,y=o.high,m=o.low,_=u.high,S=u.low,b=c.high,w=c.low,F=f.high,E=f.low,x=h.high,A=h.low,k=l.high,P=l.low,C=p,T=d,R=g,I=v,O=y,D=m,N=_,L=S,M=b,j=w,U=F,B=E,H=x,V=A,K=k,q=P,W=0;80>W;W++){var J=a[W];if(16>W)var z=J.high=0|e[r+2*W],Y=J.low=0|e[r+2*W+1];else{z=((Y=(z=a[W-15]).high)>>>1|(G=z.low)<<31)^(Y>>>8|G<<24)^Y>>>7;var G=(G>>>1|Y<<31)^(G>>>8|Y<<24)^(G>>>7|Y<<25),X=((Y=(X=a[W-2]).high)>>>19|($=X.low)<<13)^(Y<<3|$>>>29)^Y>>>6,$=($>>>19|Y<<13)^($<<3|Y>>>29)^($>>>6|Y<<26),Q=(Y=a[W-7]).high,Z=(tt=a[W-16]).high,tt=tt.low;z=(z=(z=z+Q+((Y=G+Y.low)>>>0>>0?1:0))+X+((Y=Y+$)>>>0<$>>>0?1:0))+Z+((Y=Y+tt)>>>0>>0?1:0);J.high=z,J.low=Y}Q=M&U^~M&H,tt=j&B^~j&V,J=C&R^C&O^R&O;var et=T&I^T&D^I&D,rt=(G=(C>>>28|T<<4)^(C<<30|T>>>2)^(C<<25|T>>>7),X=(T>>>28|C<<4)^(T<<30|C>>>2)^(T<<25|C>>>7),($=s[W]).high),nt=$.low;Z=(Z=(Z=(Z=K+((M>>>14|j<<18)^(M>>>18|j<<14)^(M<<23|j>>>9))+(($=q+((j>>>14|M<<18)^(j>>>18|M<<14)^(j<<23|M>>>9)))>>>0>>0?1:0))+Q+(($=$+tt)>>>0>>0?1:0))+rt+(($=$+nt)>>>0>>0?1:0))+z+(($=$+Y)>>>0>>0?1:0),J=G+J+((Y=X+et)>>>0>>0?1:0),K=H,q=V,H=U,V=B,U=M,B=j,M=N+Z+((j=L+$|0)>>>0>>0?1:0)|0,N=O,L=D,O=R,D=I,R=C,I=T,C=Z+J+((T=$+Y|0)>>>0<$>>>0?1:0)|0}d=n.low=d+T,n.high=p+C+(d>>>0>>0?1:0),v=i.low=v+I,i.high=g+R+(v>>>0>>0?1:0),m=o.low=m+D,o.high=y+O+(m>>>0>>0?1:0),S=u.low=S+L,u.high=_+N+(S>>>0>>0?1:0),w=c.low=w+j,c.high=b+M+(w>>>0>>0?1:0),E=f.low=E+B,f.high=F+U+(E>>>0>>0?1:0),A=h.low=A+V,h.high=x+H+(A>>>0>>0?1:0),P=l.low=P+q,l.high=k+K+(P>>>0>>0?1:0)},_doFinalize:function t(){var e=this._data,r=e.words,n=8*this._nDataBytes,i=8*e.sigBytes;return r[i>>>5]|=128<<24-i%32,r[30+(i+128>>>10<<5)]=Math.floor(n/4294967296),r[31+(i+128>>>10<<5)]=n,e.sigBytes=4*r.length,this._process(),this._hash.toX32()},clone:function t(){var e=r.clone.call(this);return e._hash=this._hash.clone(),e},blockSize:32}),e.SHA512=r._createHelper(o),e.HmacSHA512=r._createHmacHelper(o)}(),function(){var t=y,e=(i=t.x64).Word,r=i.WordArray,n=(i=t.algo).SHA512,i=i.SHA384=n.extend({_doReset:function t(){this._hash=new r.init([new e.init(3418070365,3238371032),new e.init(1654270250,914150663),new e.init(2438529370,812702999),new e.init(355462360,4144912697),new e.init(1731405415,4290775857),new e.init(2394180231,1750603025),new e.init(3675008525,1694076839),new e.init(1203062813,3204075428)])},_doFinalize:function t(){var e=n._doFinalize.call(this);return e.sigBytes-=16,e}});t.SHA384=n._createHelper(i),t.HmacSHA384=n._createHmacHelper(i)}(); +/*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ +var m,_="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",S="=";function b(t){var e,r,n="";for(e=0;e+3<=t.length;e+=3)r=parseInt(t.substring(e,e+3),16),n+=_.charAt(r>>6)+_.charAt(63&r);if(e+1==t.length?(r=parseInt(t.substring(e,e+1),16),n+=_.charAt(r<<2)):e+2==t.length&&(r=parseInt(t.substring(e,e+2),16),n+=_.charAt(r>>2)+_.charAt((3&r)<<4)),S)for(;(3&n.length)>0;)n+=S;return n}function w(t){var e,r,n,i="",o=0;for(e=0;e>2),r=3&n,o=1):1==o?(i+=O(r<<2|n>>4),r=15&n,o=2):2==o?(i+=O(r),i+=O(n>>2),r=3&n,o=3):(i+=O(r<<2|n>>4),i+=O(15&n),o=0));return 1==o&&(i+=O(r<<2)),i}function F(t){var e,r=w(t),n=new Array;for(e=0;2*e>15;--o>=0;){var u=32767&this[t],c=this[t++]>>15,f=a*u+c*s;i=((u=s*u+((32767&f)<<15)+r[n]+(1073741823&i))>>>30)+(f>>>15)+a*c+(i>>>30),r[n++]=1073741823&u}return i},m=30):"Netscape"!=n.appName?(E.prototype.am=function k(t,e,r,n,i,o){for(;--o>=0;){var s=e*this[t++]+r[n]+i;i=Math.floor(s/67108864),r[n++]=67108863&s}return i},m=26):(E.prototype.am=function P(t,e,r,n,i,o){for(var s=16383&e,a=e>>14;--o>=0;){var u=16383&this[t],c=this[t++]>>14,f=a*u+c*s;i=((u=s*u+((16383&f)<<14)+r[n]+i)>>28)+(f>>14)+a*c,r[n++]=268435455&u}return i},m=28),E.prototype.DB=m,E.prototype.DM=(1<>>16)&&(t=e,r+=16),0!=(e=t>>8)&&(t=e,r+=8),0!=(e=t>>4)&&(t=e,r+=4),0!=(e=t>>2)&&(t=e,r+=2),0!=(e=t>>1)&&(t=e,r+=1),r}function M(t){this.m=t}function j(t){this.m=t,this.mp=t.invDigit(),this.mpl=32767&this.mp,this.mph=this.mp>>15,this.um=(1<>=16,e+=16),0==(255&t)&&(t>>=8,e+=8),0==(15&t)&&(t>>=4,e+=4),0==(3&t)&&(t>>=2,e+=2),0==(1&t)&&++e,e}function q(t){for(var e=0;0!=t;)t&=t-1,++e;return e}function W(){}function J(t){return t}function z(t){this.r2=x(),this.q3=x(),E.ONE.dlShiftTo(2*t.t,this.r2),this.mu=this.r2.divide(t),this.m=t}M.prototype.convert=function Y(t){return t.s<0||t.compareTo(this.m)>=0?t.mod(this.m):t},M.prototype.revert=function G(t){return t},M.prototype.reduce=function X(t){t.divRemTo(this.m,null,t)},M.prototype.mulTo=function $(t,e,r){t.multiplyTo(e,r),this.reduce(r)},M.prototype.sqrTo=function Q(t,e){t.squareTo(e),this.reduce(e)},j.prototype.convert=function Z(t){var e=x();return t.abs().dlShiftTo(this.m.t,e),e.divRemTo(this.m,null,e),t.s<0&&e.compareTo(E.ZERO)>0&&this.m.subTo(e,e),e},j.prototype.revert=function tt(t){var e=x();return t.copyTo(e),this.reduce(e),e},j.prototype.reduce=function et(t){for(;t.t<=this.mt2;)t[t.t++]=0;for(var e=0;e>15)*this.mpl&this.um)<<15)&t.DM;for(t[r=e+this.m.t]+=this.m.am(0,n,t,e,0,this.m.t);t[r]>=t.DV;)t[r]-=t.DV,t[++r]++}t.clamp(),t.drShiftTo(this.m.t,t),t.compareTo(this.m)>=0&&t.subTo(this.m,t)},j.prototype.mulTo=function rt(t,e,r){t.multiplyTo(e,r),this.reduce(r)},j.prototype.sqrTo=function nt(t,e){t.squareTo(e),this.reduce(e)},E.prototype.copyTo=function it(t){for(var e=this.t-1;e>=0;--e)t[e]=this[e];t.t=this.t,t.s=this.s},E.prototype.fromInt=function ot(t){this.t=1,this.s=t<0?-1:0,t>0?this[0]=t:t<-1?this[0]=t+this.DV:this.t=0},E.prototype.fromString=function st(t,e){var r;if(16==e)r=4;else if(8==e)r=3;else if(256==e)r=8;else if(2==e)r=1;else if(32==e)r=5;else{if(4!=e)return void this.fromRadix(t,e);r=2}this.t=0,this.s=0;for(var n=t.length,i=!1,o=0;--n>=0;){var s=8==r?255&t[n]:D(t,n);s<0?"-"==t.charAt(n)&&(i=!0):(i=!1,0==o?this[this.t++]=s:o+r>this.DB?(this[this.t-1]|=(s&(1<>this.DB-o):this[this.t-1]|=s<=this.DB&&(o-=this.DB))}8==r&&0!=(128&t[0])&&(this.s=-1,o>0&&(this[this.t-1]|=(1<0&&this[this.t-1]==t;)--this.t},E.prototype.dlShiftTo=function ut(t,e){var r;for(r=this.t-1;r>=0;--r)e[r+t]=this[r];for(r=t-1;r>=0;--r)e[r]=0;e.t=this.t+t,e.s=this.s},E.prototype.drShiftTo=function ct(t,e){for(var r=t;r=0;--r)e[r+s+1]=this[r]>>i|a,a=(this[r]&o)<=0;--r)e[r]=0;e[s]=a,e.t=this.t+s+1,e.s=this.s,e.clamp()},E.prototype.rShiftTo=function ht(t,e){e.s=this.s;var r=Math.floor(t/this.DB);if(r>=this.t)e.t=0;else{var n=t%this.DB,i=this.DB-n,o=(1<>n;for(var s=r+1;s>n;n>0&&(e[this.t-r-1]|=(this.s&o)<>=this.DB;if(t.t>=this.DB;n+=this.s}else{for(n+=this.s;r>=this.DB;n-=t.s}e.s=n<0?-1:0,n<-1?e[r++]=this.DV+n:n>0&&(e[r++]=n),e.t=r,e.clamp()},E.prototype.multiplyTo=function pt(t,e){var r=this.abs(),n=t.abs(),i=r.t;for(e.t=i+n.t;--i>=0;)e[i]=0;for(i=0;i=0;)t[r]=0;for(r=0;r=e.DV&&(t[r+e.t]-=e.DV,t[r+e.t+1]=1)}t.t>0&&(t[t.t-1]+=e.am(r,e[r],t,2*r,0,1)),t.s=0,t.clamp()},E.prototype.divRemTo=function gt(t,e,r){var n=t.abs();if(!(n.t<=0)){var i=this.abs();if(i.t0?(n.lShiftTo(u,o),i.lShiftTo(u,r)):(n.copyTo(o),i.copyTo(r));var c=o.t,f=o[c-1];if(0!=f){var h=f*(1<1?o[c-2]>>this.F2:0),l=this.FV/h,p=(1<=0&&(r[r.t++]=1,r.subTo(y,r)),E.ONE.dlShiftTo(c,y),y.subTo(o,o);o.t=0;){var m=r[--g]==f?this.DM:Math.floor(r[g]*l+(r[g-1]+d)*p);if((r[g]+=o.am(0,m,r,v,0,c))0&&r.rShiftTo(u,r),s<0&&E.ZERO.subTo(r,r)}}},E.prototype.invDigit=function vt(){if(this.t<1)return 0;var t=this[0];if(0==(1&t))return 0;var e=3&t;return(e=(e=(e=(e=e*(2-(15&t)*e)&15)*(2-(255&t)*e)&255)*(2-((65535&t)*e&65535))&65535)*(2-t*e%this.DV)%this.DV)>0?this.DV-e:-e},E.prototype.isEven=function yt(){return 0==(this.t>0?1&this[0]:this.s)},E.prototype.exp=function mt(t,e){if(t>4294967295||t<1)return E.ONE;var r=x(),n=x(),i=e.convert(this),o=L(t)-1;for(i.copyTo(r);--o>=0;)if(e.sqrTo(r,n),(t&1<0)e.mulTo(n,i,r);else{var s=r;r=n,n=s}return e.revert(r)},E.prototype.toString=function _t(t){if(this.s<0)return"-"+this.negate().toString(t);var e;if(16==t)e=4;else if(8==t)e=3;else if(2==t)e=1;else if(32==t)e=5;else{if(4!=t)return this.toRadix(t);e=2}var r,n=(1<0)for(a>a)>0&&(i=!0,o=O(r));s>=0;)a>(a+=this.DB-e)):(r=this[s]>>(a-=e)&n,a<=0&&(a+=this.DB,--s)),r>0&&(i=!0),i&&(o+=O(r));return i?o:"0"},E.prototype.negate=function St(){var t=x();return E.ZERO.subTo(this,t),t},E.prototype.abs=function bt(){return this.s<0?this.negate():this},E.prototype.compareTo=function wt(t){var e=this.s-t.s;if(0!=e)return e;var r=this.t;if(0!=(e=r-t.t))return this.s<0?-e:e;for(;--r>=0;)if(0!=(e=this[r]-t[r]))return e;return 0},E.prototype.bitLength=function Ft(){return this.t<=0?0:this.DB*(this.t-1)+L(this[this.t-1]^this.s&this.DM)},E.prototype.mod=function Et(t){var e=x();return this.abs().divRemTo(t,null,e),this.s<0&&e.compareTo(E.ZERO)>0&&t.subTo(e,e),e},E.prototype.modPowInt=function xt(t,e){var r;return r=t<256||e.isEven()?new M(e):new j(e),this.exp(t,r)},E.ZERO=N(0),E.ONE=N(1),W.prototype.convert=J,W.prototype.revert=J,W.prototype.mulTo=function At(t,e,r){t.multiplyTo(e,r)},W.prototype.sqrTo=function kt(t,e){t.squareTo(e)},z.prototype.convert=function Pt(t){if(t.s<0||t.t>2*this.m.t)return t.mod(this.m);if(t.compareTo(this.m)<0)return t;var e=x();return t.copyTo(e),this.reduce(e),e},z.prototype.revert=function Ct(t){return t},z.prototype.reduce=function Tt(t){for(t.drShiftTo(this.m.t-1,this.r2),t.t>this.m.t+1&&(t.t=this.m.t+1,t.clamp()),this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3),this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2);t.compareTo(this.r2)<0;)t.dAddOffset(1,this.m.t+1);for(t.subTo(this.r2,t);t.compareTo(this.m)>=0;)t.subTo(this.m,t)},z.prototype.mulTo=function Rt(t,e,r){t.multiplyTo(e,r),this.reduce(r)},z.prototype.sqrTo=function It(t,e){t.squareTo(e),this.reduce(e)};var Ot=[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997],Dt=(1<<26)/Ot[Ot.length-1]; +/*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ +function Nt(){this.i=0,this.j=0,this.S=new Array}E.prototype.chunkSize=function Lt(t){return Math.floor(Math.LN2*this.DB/Math.log(t))},E.prototype.toRadix=function Mt(t){if(null==t&&(t=10),0==this.signum()||t<2||t>36)return"0";var e=this.chunkSize(t),r=Math.pow(t,e),n=N(r),i=x(),o=x(),s="";for(this.divRemTo(n,i,o);i.signum()>0;)s=(r+o.intValue()).toString(t).substr(1)+s,i.divRemTo(n,i,o);return o.intValue().toString(t)+s},E.prototype.fromRadix=function jt(t,e){this.fromInt(0),null==e&&(e=10);for(var r=this.chunkSize(e),n=Math.pow(e,r),i=!1,o=0,s=0,a=0;a=r&&(this.dMultiply(n),this.dAddOffset(s,0),o=0,s=0))}o>0&&(this.dMultiply(Math.pow(e,o)),this.dAddOffset(s,0)),i&&E.ZERO.subTo(this,this)},E.prototype.fromNumber=function Ut(t,e,r){if("number"==typeof e)if(t<2)this.fromInt(1);else for(this.fromNumber(t,r),this.testBit(t-1)||this.bitwiseTo(E.ONE.shiftLeft(t-1),B,this),this.isEven()&&this.dAddOffset(1,0);!this.isProbablePrime(e);)this.dAddOffset(2,0),this.bitLength()>t&&this.subTo(E.ONE.shiftLeft(t-1),this);else{var n=new Array,i=7&t;n.length=1+(t>>3),e.nextBytes(n),i>0?n[0]&=(1<>=this.DB;if(t.t>=this.DB;n+=this.s}else{for(n+=this.s;r>=this.DB;n+=t.s}e.s=n<0?-1:0,n>0?e[r++]=n:n<-1&&(e[r++]=this.DV+n),e.t=r,e.clamp()},E.prototype.dMultiply=function Kt(t){this[this.t]=this.am(0,t-1,this,0,0,this.t),++this.t,this.clamp()},E.prototype.dAddOffset=function qt(t,e){if(0!=t){for(;this.t<=e;)this[this.t++]=0;for(this[e]+=t;this[e]>=this.DV;)this[e]-=this.DV,++e>=this.t&&(this[this.t++]=0),++this[e]}},E.prototype.multiplyLowerTo=function Wt(t,e,r){var n,i=Math.min(this.t+t.t,e);for(r.s=0,r.t=i;i>0;)r[--i]=0;for(n=r.t-this.t;i=0;)r[n]=0;for(n=Math.max(e-this.t,0);n0)if(0==e)r=this[0]%t;else for(var n=this.t-1;n>=0;--n)r=(e*r+this[n])%t;return r},E.prototype.millerRabin=function Yt(t){var e=this.subtract(E.ONE),r=e.getLowestSetBit();if(r<=0)return!1;var n=e.shiftRight(r);(t=t+1>>1)>Ot.length&&(t=Ot.length);for(var i=x(),o=0;o>24},E.prototype.shortValue=function Qt(){return 0==this.t?this.s:this[0]<<16>>16},E.prototype.signum=function Zt(){return this.s<0?-1:this.t<=0||1==this.t&&this[0]<=0?0:1},E.prototype.toByteArray=function te(){var t=this.t,e=new Array;e[0]=this.s;var r,n=this.DB-t*this.DB%8,i=0;if(t-- >0)for(n>n)!=(this.s&this.DM)>>n&&(e[i++]=r|this.s<=0;)n<8?(r=(this[t]&(1<>(n+=this.DB-8)):(r=this[t]>>(n-=8)&255,n<=0&&(n+=this.DB,--t)),0!=(128&r)&&(r|=-256),0==i&&(128&this.s)!=(128&r)&&++i,(i>0||r!=this.s)&&(e[i++]=r);return e},E.prototype.equals=function ee(t){return 0==this.compareTo(t)},E.prototype.min=function re(t){return this.compareTo(t)<0?this:t},E.prototype.max=function ne(t){return this.compareTo(t)>0?this:t},E.prototype.and=function ie(t){var e=x();return this.bitwiseTo(t,U,e),e},E.prototype.or=function oe(t){var e=x();return this.bitwiseTo(t,B,e),e},E.prototype.xor=function se(t){var e=x();return this.bitwiseTo(t,H,e),e},E.prototype.andNot=function ae(t){var e=x();return this.bitwiseTo(t,V,e),e},E.prototype.not=function ue(){for(var t=x(),e=0;e=this.t?0!=this.s:0!=(this[e]&1<1){var f=x();for(n.sqrTo(s[1],f);a<=c;)s[a]=x(),n.mulTo(f,s[a-2],s[a]),a+=2}var h,l,p=t.t-1,d=!0,g=x();for(i=L(t[p])-1;p>=0;){for(i>=u?h=t[p]>>i-u&c:(h=(t[p]&(1<0&&(h|=t[p-1]>>this.DB+i-u)),a=r;0==(1&h);)h>>=1,--a;if((i-=a)<0&&(i+=this.DB,--p),d)s[h].copyTo(o),d=!1;else{for(;a>1;)n.sqrTo(o,g),n.sqrTo(g,o),a-=2;a>0?n.sqrTo(o,g):(l=o,o=g,g=l),n.mulTo(g,s[h],o)}for(;p>=0&&0==(t[p]&1<=0?(r.subTo(n,r),e&&i.subTo(s,i),o.subTo(a,o)):(n.subTo(r,n),e&&s.subTo(i,s),a.subTo(o,a))}return 0!=n.compareTo(E.ONE)?E.ZERO:a.compareTo(t)>=0?a.subtract(t):a.signum()<0?(a.addTo(t,a),a.signum()<0?a.add(t):a):a},E.prototype.pow=function xe(t){return this.exp(t,new W)},E.prototype.gcd=function Ae(t){var e=this.s<0?this.negate():this.clone(),r=t.s<0?t.negate():t.clone();if(e.compareTo(r)<0){var n=e;e=r,r=n}var i=e.getLowestSetBit(),o=r.getLowestSetBit();if(o<0)return e;for(i0&&(e.rShiftTo(o,e),r.rShiftTo(o,r));e.signum()>0;)(i=e.getLowestSetBit())>0&&e.rShiftTo(i,e),(i=r.getLowestSetBit())>0&&r.rShiftTo(i,r),e.compareTo(r)>=0?(e.subTo(r,e),e.rShiftTo(1,e)):(r.subTo(e,r),r.rShiftTo(1,r));return o>0&&r.lShiftTo(o,r),r},E.prototype.isProbablePrime=function ke(t){var e,r=this.abs();if(1==r.t&&r[0]<=Ot[Ot.length-1]){for(e=0;e>8&255,Ie[Oe++]^=e>>16&255,Ie[Oe++]^=e>>24&255,Oe>=De&&(Oe-=De)}((new Date).getTime())}if(null==Ie){var Le;if(Ie=new Array,Oe=0,void 0!==i&&(void 0!==i.crypto||void 0!==i.msCrypto)){var Me=i.crypto||i.msCrypto;if(Me.getRandomValues){var je=new Uint8Array(32);for(Me.getRandomValues(je),Le=0;Le<32;++Le)Ie[Oe++]=je[Le]}else if("Netscape"==n.appName&&n.appVersion<"5"){var Ue=i.crypto.random(32);for(Le=0;Le>>8,Ie[Oe++]=255&Le;Oe=0,Ne()}function Be(){if(null==Re){for(Ne(),(Re=function t(){return new Nt}()).init(Ie),Oe=0;Oe>24,(16711680&i)>>16,(65280&i)>>8,255&i]))),i+=1;return n}function qe(){this.n=null,this.e=0,this.d=null,this.p=null,this.q=null,this.dmp1=null,this.dmq1=null,this.coeff=null} +/*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ +function We(t,e){this.x=e,this.q=t}function Je(t,e,r,n){this.curve=t,this.x=e,this.y=r,this.z=null==n?E.ONE:n,this.zinv=null}function ze(t,e,r){this.q=t,this.a=this.fromBigInteger(e),this.b=this.fromBigInteger(r),this.infinity=new Je(this,null,null)}He.prototype.nextBytes=function Ye(t){var e;for(e=0;e0&&e.length>0))throw"Invalid RSA public key";this.n=Ve(t,16),this.e=parseInt(e,16)}},qe.prototype.encrypt=function $e(t){var e=function r(t,e){if(e=0&&e>0;){var i=t.charCodeAt(n--);i<128?r[--e]=i:i>127&&i<2048?(r[--e]=63&i|128,r[--e]=i>>6|192):(r[--e]=63&i|128,r[--e]=i>>6&63|128,r[--e]=i>>12|224)}r[--e]=0;for(var o=new He,s=new Array;e>2;){for(s[0]=0;0==s[0];)o.nextBytes(s);r[--e]=s[0]}return r[--e]=2,r[--e]=0,new E(r)}(t,this.n.bitLength()+7>>3);if(null==e)return null;var n=this.doPublic(e);if(null==n)return null;var i=n.toString(16);return 0==(1&i.length)?i:"0"+i},qe.prototype.encryptOAEP=function Qe(t,e,r){var n=function i(t,e,r,n){var i=Er.crypto.MessageDigest,o=Er.crypto.Util,s=null;if(r||(r="sha1"),"string"==typeof r&&(s=i.getCanonicalAlgName(r),n=i.getHashLength(s),r=function t(e){return jr(o.hashHex(Ur(e),s))}),t.length+2*n+2>e)throw"Message too long for RSA";var a,u="";for(a=0;a>3,e,r);if(null==n)return null;var o=this.doPublic(n);if(null==o)return null;var s=o.toString(16);return 0==(1&s.length)?s:"0"+s},qe.prototype.type="RSA",We.prototype.equals=function Ze(t){return t==this||this.q.equals(t.q)&&this.x.equals(t.x)},We.prototype.toBigInteger=function tr(){return this.x},We.prototype.negate=function er(){return new We(this.q,this.x.negate().mod(this.q))},We.prototype.add=function rr(t){return new We(this.q,this.x.add(t.toBigInteger()).mod(this.q))},We.prototype.subtract=function nr(t){return new We(this.q,this.x.subtract(t.toBigInteger()).mod(this.q))},We.prototype.multiply=function ir(t){return new We(this.q,this.x.multiply(t.toBigInteger()).mod(this.q))},We.prototype.square=function or(){return new We(this.q,this.x.square().mod(this.q))},We.prototype.divide=function sr(t){return new We(this.q,this.x.multiply(t.toBigInteger().modInverse(this.q)).mod(this.q))},Je.prototype.getX=function ar(){return null==this.zinv&&(this.zinv=this.z.modInverse(this.curve.q)),this.curve.fromBigInteger(this.x.toBigInteger().multiply(this.zinv).mod(this.curve.q))},Je.prototype.getY=function ur(){return null==this.zinv&&(this.zinv=this.z.modInverse(this.curve.q)),this.curve.fromBigInteger(this.y.toBigInteger().multiply(this.zinv).mod(this.curve.q))},Je.prototype.equals=function cr(t){return t==this||(this.isInfinity()?t.isInfinity():t.isInfinity()?this.isInfinity():!!t.y.toBigInteger().multiply(this.z).subtract(this.y.toBigInteger().multiply(t.z)).mod(this.curve.q).equals(E.ZERO)&&t.x.toBigInteger().multiply(this.z).subtract(this.x.toBigInteger().multiply(t.z)).mod(this.curve.q).equals(E.ZERO))},Je.prototype.isInfinity=function fr(){return null==this.x&&null==this.y||this.z.equals(E.ZERO)&&!this.y.toBigInteger().equals(E.ZERO)},Je.prototype.negate=function hr(){return new Je(this.curve,this.x,this.y.negate(),this.z)},Je.prototype.add=function lr(t){if(this.isInfinity())return t;if(t.isInfinity())return this;var e=t.y.toBigInteger().multiply(this.z).subtract(this.y.toBigInteger().multiply(t.z)).mod(this.curve.q),r=t.x.toBigInteger().multiply(this.z).subtract(this.x.toBigInteger().multiply(t.z)).mod(this.curve.q);if(E.ZERO.equals(r))return E.ZERO.equals(e)?this.twice():this.curve.getInfinity();var n=new E("3"),i=this.x.toBigInteger(),o=this.y.toBigInteger(),s=(t.x.toBigInteger(),t.y.toBigInteger(),r.square()),a=s.multiply(r),u=i.multiply(s),c=e.square().multiply(this.z),f=c.subtract(u.shiftLeft(1)).multiply(t.z).subtract(a).multiply(r).mod(this.curve.q),h=u.multiply(n).multiply(e).subtract(o.multiply(a)).subtract(c.multiply(e)).multiply(t.z).add(e.multiply(a)).mod(this.curve.q),l=a.multiply(this.z).multiply(t.z).mod(this.curve.q);return new Je(this.curve,this.curve.fromBigInteger(f),this.curve.fromBigInteger(h),l)},Je.prototype.twice=function pr(){if(this.isInfinity())return this;if(0==this.y.toBigInteger().signum())return this.curve.getInfinity();var t=new E("3"),e=this.x.toBigInteger(),r=this.y.toBigInteger(),n=r.multiply(this.z),i=n.multiply(r).mod(this.curve.q),o=this.curve.a.toBigInteger(),s=e.square().multiply(t);E.ZERO.equals(o)||(s=s.add(this.z.square().multiply(o)));var a=(s=s.mod(this.curve.q)).square().subtract(e.shiftLeft(3).multiply(i)).shiftLeft(1).multiply(n).mod(this.curve.q),u=s.multiply(t).multiply(e).subtract(i.shiftLeft(1)).shiftLeft(2).multiply(i).subtract(s.square().multiply(s)).mod(this.curve.q),c=n.square().multiply(n).shiftLeft(3).mod(this.curve.q);return new Je(this.curve,this.curve.fromBigInteger(a),this.curve.fromBigInteger(u),c)},Je.prototype.multiply=function dr(t){if(this.isInfinity())return this;if(0==t.signum())return this.curve.getInfinity();var e,r=t,n=r.multiply(new E("3")),i=this.negate(),o=this;for(e=n.bitLength()-2;e>0;--e){o=o.twice();var s=n.testBit(e);s!=r.testBit(e)&&(o=o.add(s?this:i))}return o},Je.prototype.multiplyTwo=function gr(t,e,r){var n;n=t.bitLength()>r.bitLength()?t.bitLength()-1:r.bitLength()-1;for(var i=this.curve.getInfinity(),o=this.add(e);n>=0;)i=i.twice(),t.testBit(n)?i=r.testBit(n)?i.add(o):i.add(this):r.testBit(n)&&(i=i.add(e)),--n;return i},ze.prototype.getQ=function vr(){return this.q},ze.prototype.getA=function yr(){return this.a},ze.prototype.getB=function mr(){return this.b},ze.prototype.equals=function _r(t){return t==this||this.q.equals(t.q)&&this.a.equals(t.a)&&this.b.equals(t.b)},ze.prototype.getInfinity=function Sr(){return this.infinity},ze.prototype.fromBigInteger=function br(t){return new We(this.q,t)},ze.prototype.decodePointHex=function wr(t){switch(parseInt(t.substr(0,2),16)){case 0:return this.infinity;case 2:case 3:return null;case 4:case 6:case 7:var e=(t.length-2)/2,r=t.substr(2,e),n=t.substr(e+2,e);return new Je(this,this.fromBigInteger(new E(r,16)),this.fromBigInteger(new E(n,16)));default:return null}}, +/*! (c) Stefan Thomas | https://github.com/bitcoinjs/bitcoinjs-lib + */ +We.prototype.getByteLength=function(){return Math.floor((this.toBigInteger().bitLength()+7)/8)},Je.prototype.getEncoded=function(t){var e=function t(e,r){var n=e.toByteArrayUnsigned();if(rn.length;)n.unshift(0);return n},r=this.getX().toBigInteger(),n=this.getY().toBigInteger(),i=e(r,32);return t?n.isEven()?i.unshift(2):i.unshift(3):(i.unshift(4),i=i.concat(e(n,32))),i},Je.decodeFrom=function(t,e){e[0];var r=e.length-1,n=e.slice(1,1+r/2),i=e.slice(1+r/2,1+r);n.unshift(0),i.unshift(0);var o=new E(n),s=new E(i);return new Je(t,t.fromBigInteger(o),t.fromBigInteger(s))},Je.decodeFromHex=function(t,e){e.substr(0,2);var r=e.length-2,n=e.substr(2,r/2),i=e.substr(2+r/2,r/2),o=new E(n,16),s=new E(i,16);return new Je(t,t.fromBigInteger(o),t.fromBigInteger(s))},Je.prototype.add2D=function(t){if(this.isInfinity())return t;if(t.isInfinity())return this;if(this.x.equals(t.x))return this.y.equals(t.y)?this.twice():this.curve.getInfinity();var e=t.x.subtract(this.x),r=t.y.subtract(this.y).divide(e),n=r.square().subtract(this.x).subtract(t.x),i=r.multiply(this.x.subtract(n)).subtract(this.y);return new Je(this.curve,n,i)},Je.prototype.twice2D=function(){if(this.isInfinity())return this;if(0==this.y.toBigInteger().signum())return this.curve.getInfinity();var t=this.curve.fromBigInteger(E.valueOf(2)),e=this.curve.fromBigInteger(E.valueOf(3)),r=this.x.square().multiply(e).add(this.curve.a).divide(this.y.multiply(t)),n=r.square().subtract(this.x.multiply(t)),i=r.multiply(this.x.subtract(n)).subtract(this.y);return new Je(this.curve,n,i)},Je.prototype.multiply2D=function(t){if(this.isInfinity())return this;if(0==t.signum())return this.curve.getInfinity();var e,r=t,n=r.multiply(new E("3")),i=this.negate(),o=this;for(e=n.bitLength()-2;e>0;--e){o=o.twice();var s=n.testBit(e);s!=r.testBit(e)&&(o=o.add2D(s?this:i))}return o},Je.prototype.isOnCurve=function(){var t=this.getX().toBigInteger(),e=this.getY().toBigInteger(),r=this.curve.getA().toBigInteger(),n=this.curve.getB().toBigInteger(),i=this.curve.getQ(),o=e.multiply(e).mod(i),s=t.multiply(t).multiply(t).add(r.multiply(t)).add(n).mod(i);return o.equals(s)},Je.prototype.toString=function(){return"("+this.getX().toBigInteger().toString()+","+this.getY().toBigInteger().toString()+")"},Je.prototype.validate=function(){var t=this.curve.getQ();if(this.isInfinity())throw new Error("Point is at infinity.");var e=this.getX().toBigInteger(),r=this.getY().toBigInteger();if(e.compareTo(E.ONE)<0||e.compareTo(t.subtract(E.ONE))>0)throw new Error("x coordinate out of bounds");if(r.compareTo(E.ONE)<0||r.compareTo(t.subtract(E.ONE))>0)throw new Error("y coordinate out of bounds");if(!this.isOnCurve())throw new Error("Point is not on the curve.");if(this.multiply(t).isInfinity())throw new Error("Point is not a scalar multiple of G.");return!0}; +/*! Mike Samuel (c) 2009 | code.google.com/p/json-sans-eval + */ +var Fr=function(){var t=new RegExp('(?:false|true|null|[\\{\\}\\[\\]]|(?:-?\\b(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\\b)|(?:"(?:[^\\0-\\x08\\x0a-\\x1f"\\\\]|\\\\(?:["/\\\\bfnrt]|u[0-9A-Fa-f]{4}))*"))',"g"),e=new RegExp("\\\\(?:([^u])|u(.{4}))","g"),n={'"':'"',"/":"/","\\":"\\",b:"\b",f:"\f",n:"\n",r:"\r",t:"\t"};function i(t,e,r){return e?n[e]:String.fromCharCode(parseInt(r,16))}var o=new String(""),s=(Object,Array,Object.hasOwnProperty);return function(n,a){var u,c,f=n.match(t),h=f[0],l=!1;"{"===h?u={}:"["===h?u=[]:(u=[],l=!0);for(var p=[u],d=1-l,g=f.length;d=0;)delete i[o[f]]}return a.call(e,n,i)}({"":u},"")}return u}}();void 0!==Er&&Er||(e.KJUR=Er={}),void 0!==Er.asn1&&Er.asn1||(Er.asn1={}),Er.asn1.ASN1Util=new function(){this.integerToByteHex=function(t){var e=t.toString(16);return e.length%2==1&&(e="0"+e),e},this.bigIntToMinTwosComplementsHex=function(t){var e=t.toString(16);if("-"!=e.substr(0,1))e.length%2==1?e="0"+e:e.match(/^[0-7]/)||(e="00"+e);else{var r=e.substr(1).length;r%2==1?r+=1:e.match(/^[0-7]/)||(r+=2);for(var n="",i=0;i15)throw"ASN.1 length too long to represent by 8x: n = "+t.toString(16);return(128+r).toString(16)+e},this.getEncodedHex=function(){return(null==this.hTLV||this.isModified)&&(this.hV=this.getFreshValueHex(),this.hL=this.getLengthHexFromValue(),this.hTLV=this.hT+this.hL+this.hV,this.isModified=!1),this.hTLV},this.getValueHex=function(){return this.getEncodedHex(),this.hV},this.getFreshValueHex=function(){return""}},Er.asn1.DERAbstractString=function(t){Er.asn1.DERAbstractString.superclass.constructor.call(this);this.getString=function(){return this.s},this.setString=function(t){this.hTLV=null,this.isModified=!0,this.s=t,this.hV=Lr(this.s).toLowerCase()},this.setStringHex=function(t){this.hTLV=null,this.isModified=!0,this.s=null,this.hV=t},this.getFreshValueHex=function(){return this.hV},void 0!==t&&("string"==typeof t?this.setString(t):void 0!==t.str?this.setString(t.str):void 0!==t.hex&&this.setStringHex(t.hex))},o.lang.extend(Er.asn1.DERAbstractString,Er.asn1.ASN1Object),Er.asn1.DERAbstractTime=function(t){Er.asn1.DERAbstractTime.superclass.constructor.call(this);this.localDateToUTC=function(t){return utc=t.getTime()+6e4*t.getTimezoneOffset(),new Date(utc)},this.formatDate=function(t,e,r){var n=this.zeroPadding,i=this.localDateToUTC(t),o=String(i.getFullYear());"utc"==e&&(o=o.substr(2,2));var s=o+n(String(i.getMonth()+1),2)+n(String(i.getDate()),2)+n(String(i.getHours()),2)+n(String(i.getMinutes()),2)+n(String(i.getSeconds()),2);if(!0===r){var a=i.getMilliseconds();if(0!=a){var u=n(String(a),3);s=s+"."+(u=u.replace(/[0]+$/,""))}}return s+"Z"},this.zeroPadding=function(t,e){return t.length>=e?t:new Array(e-t.length+1).join("0")+t},this.getString=function(){return this.s},this.setString=function(t){this.hTLV=null,this.isModified=!0,this.s=t,this.hV=Rr(t)},this.setByDateValue=function(t,e,r,n,i,o){var s=new Date(Date.UTC(t,e-1,r,n,i,o,0));this.setByDate(s)},this.getFreshValueHex=function(){return this.hV}},o.lang.extend(Er.asn1.DERAbstractTime,Er.asn1.ASN1Object),Er.asn1.DERAbstractStructured=function(t){Er.asn1.DERAbstractString.superclass.constructor.call(this);this.setByASN1ObjectArray=function(t){this.hTLV=null,this.isModified=!0,this.asn1Array=t},this.appendASN1Object=function(t){this.hTLV=null,this.isModified=!0,this.asn1Array.push(t)},this.asn1Array=new Array,void 0!==t&&void 0!==t.array&&(this.asn1Array=t.array)},o.lang.extend(Er.asn1.DERAbstractStructured,Er.asn1.ASN1Object),Er.asn1.DERBoolean=function(){Er.asn1.DERBoolean.superclass.constructor.call(this),this.hT="01",this.hTLV="0101ff"},o.lang.extend(Er.asn1.DERBoolean,Er.asn1.ASN1Object),Er.asn1.DERInteger=function(t){Er.asn1.DERInteger.superclass.constructor.call(this),this.hT="02",this.setByBigInteger=function(t){this.hTLV=null,this.isModified=!0,this.hV=Er.asn1.ASN1Util.bigIntToMinTwosComplementsHex(t)},this.setByInteger=function(t){var e=new E(String(t),10);this.setByBigInteger(e)},this.setValueHex=function(t){this.hV=t},this.getFreshValueHex=function(){return this.hV},void 0!==t&&(void 0!==t.bigint?this.setByBigInteger(t.bigint):void 0!==t.int?this.setByInteger(t.int):"number"==typeof t?this.setByInteger(t):void 0!==t.hex&&this.setValueHex(t.hex))},o.lang.extend(Er.asn1.DERInteger,Er.asn1.ASN1Object),Er.asn1.DERBitString=function(t){if(void 0!==t&&void 0!==t.obj){var e=Er.asn1.ASN1Util.newObject(t.obj);t.hex="00"+e.getEncodedHex()}Er.asn1.DERBitString.superclass.constructor.call(this),this.hT="03",this.setHexValueIncludingUnusedBits=function(t){this.hTLV=null,this.isModified=!0,this.hV=t},this.setUnusedBitsAndHexValue=function(t,e){if(t<0||7i.length&&(i=n[r]);return(t=t.replace(i,"::")).slice(1,-1)}function $r(t){var e="malformed hex value";if(!t.match(/^([0-9A-Fa-f][0-9A-Fa-f]){1,}$/))throw e;if(8!=t.length)return 32==t.length?Xr(t):t;try{return parseInt(t.substr(0,2),16)+"."+parseInt(t.substr(2,2),16)+"."+parseInt(t.substr(4,2),16)+"."+parseInt(t.substr(6,2),16)}catch(t){throw e}}function Qr(t){for(var e=encodeURIComponent(t),r="",n=0;n"7"?"00"+t:t}kr.getLblen=function(t,e){if("8"!=t.substr(e+2,1))return 1;var r=parseInt(t.substr(e+3,1));return 0==r?-1:0=2*o)break;if(a>=200)break;n.push(u),s=u,a++}return n},kr.getNthChildIdx=function(t,e,r){return kr.getChildIdx(t,e)[r]},kr.getIdxbyList=function(t,e,r,n){var i,o,s=kr;if(0==r.length){if(void 0!==n&&t.substr(e,2)!==n)throw"checking tag doesn't match: "+t.substr(e,2)+"!="+n;return e}return i=r.shift(),o=s.getChildIdx(t,e),s.getIdxbyList(t,o[i],r,n)},kr.getTLVbyList=function(t,e,r,n){var i=kr,o=i.getIdxbyList(t,e,r);if(void 0===o)throw"can't find nthList object";if(void 0!==n&&t.substr(o,2)!=n)throw"checking tag doesn't match: "+t.substr(o,2)+"!="+n;return i.getTLV(t,o)},kr.getVbyList=function(t,e,r,n,i){var o,s,a=kr;if(void 0===(o=a.getIdxbyList(t,e,r,n)))throw"can't find nthList object";return s=a.getV(t,o),!0===i&&(s=s.substr(2)),s},kr.hextooidstr=function(t){var e=function t(e,r){return e.length>=r?e:new Array(r-e.length+1).join("0")+e},r=[],n=t.substr(0,2),i=parseInt(n,16);r[0]=new String(Math.floor(i/40)),r[1]=new String(i%40);for(var o=t.substr(2),s=[],a=0;a0&&(f=f+"."+u.join(".")),f},kr.dump=function(t,e,r,n){var i=kr,o=i.getV,s=i.dump,a=i.getChildIdx,u=t;t instanceof Er.asn1.ASN1Object&&(u=t.getEncodedHex());var c=function t(e,r){return e.length<=2*r?e:e.substr(0,r)+"..(total "+e.length/2+"bytes).."+e.substr(e.length-r,r)};void 0===e&&(e={ommit_long_octet:32}),void 0===r&&(r=0),void 0===n&&(n="");var f=e.ommit_long_octet;if("01"==u.substr(r,2))return"00"==(h=o(u,r))?n+"BOOLEAN FALSE\n":n+"BOOLEAN TRUE\n";if("02"==u.substr(r,2))return n+"INTEGER "+c(h=o(u,r),f)+"\n";if("03"==u.substr(r,2))return n+"BITSTRING "+c(h=o(u,r),f)+"\n";if("04"==u.substr(r,2)){var h=o(u,r);if(i.isASN1HEX(h)){var l=n+"OCTETSTRING, encapsulates\n";return l+=s(h,e,0,n+" ")}return n+"OCTETSTRING "+c(h,f)+"\n"}if("05"==u.substr(r,2))return n+"NULL\n";if("06"==u.substr(r,2)){var p=o(u,r),d=Er.asn1.ASN1Util.oidHexToInt(p),g=Er.asn1.x509.OID.oid2name(d),v=d.replace(/\./g," ");return""!=g?n+"ObjectIdentifier "+g+" ("+v+")\n":n+"ObjectIdentifier ("+v+")\n"}if("0c"==u.substr(r,2))return n+"UTF8String '"+Mr(o(u,r))+"'\n";if("13"==u.substr(r,2))return n+"PrintableString '"+Mr(o(u,r))+"'\n";if("14"==u.substr(r,2))return n+"TeletexString '"+Mr(o(u,r))+"'\n";if("16"==u.substr(r,2))return n+"IA5String '"+Mr(o(u,r))+"'\n";if("17"==u.substr(r,2))return n+"UTCTime "+Mr(o(u,r))+"\n";if("18"==u.substr(r,2))return n+"GeneralizedTime "+Mr(o(u,r))+"\n";if("30"==u.substr(r,2)){if("3000"==u.substr(r,4))return n+"SEQUENCE {}\n";l=n+"SEQUENCE\n";var y=e;if((2==(S=a(u,r)).length||3==S.length)&&"06"==u.substr(S[0],2)&&"04"==u.substr(S[S.length-1],2)){g=i.oidname(o(u,S[0]));var m=JSON.parse(JSON.stringify(e));m.x509ExtName=g,y=m}for(var _=0;_i)throw"key is too short for SigAlg: keylen="+r+","+e;for(var o="0001",s="00"+n,a="",u=i-o.length-s.length,c=0;c=0)return!1;if(r.compareTo(E.ONE)<0||r.compareTo(i)>=0)return!1;var s=r.modInverse(i),a=t.multiply(s).mod(i),u=e.multiply(s).mod(i);return o.multiply(a).add(n.multiply(u)).getX().toBigInteger().mod(i).equals(e)},this.serializeSig=function(t,e){var r=t.toByteArraySigned(),n=e.toByteArraySigned(),i=[];return i.push(2),i.push(r.length),(i=i.concat(r)).push(2),i.push(n.length),(i=i.concat(n)).unshift(i.length),i.unshift(48),i},this.parseSig=function(t){var e;if(48!=t[0])throw new Error("Signature not a valid DERSequence");if(2!=t[e=2])throw new Error("First element in signature must be a DERInteger");var r=t.slice(e+2,e+2+t[e+1]);if(2!=t[e+=2+t[e+1]])throw new Error("Second element in signature must be a DERInteger");var n=t.slice(e+2,e+2+t[e+1]);return e+=2+t[e+1],{r:E.fromByteArrayUnsigned(r),s:E.fromByteArrayUnsigned(n)}},this.parseSigCompact=function(t){if(65!==t.length)throw"Signature has the wrong length";var e=t[0]-27;if(e<0||e>7)throw"Invalid signature type";var r=this.ecparams.n;return{r:E.fromByteArrayUnsigned(t.slice(1,33)).mod(r),s:E.fromByteArrayUnsigned(t.slice(33,65)).mod(r),i:e}},this.readPKCS5PrvKeyHex=function(t){var e,r,n,i=kr,o=Er.crypto.ECDSA.getName,s=i.getVbyList;if(!1===i.isASN1HEX(t))throw"not ASN.1 hex string";try{e=s(t,0,[2,0],"06"),r=s(t,0,[1],"04");try{n=s(t,0,[3,0],"03").substr(2)}catch(t){}}catch(t){throw"malformed PKCS#1/5 plain ECC private key"}if(this.curveName=o(e),void 0===this.curveName)throw"unsupported curve name";this.setNamedCurve(this.curveName),this.setPublicKeyHex(n),this.setPrivateKeyHex(r),this.isPublic=!1},this.readPKCS8PrvKeyHex=function(t){var e,r,n,i=kr,o=Er.crypto.ECDSA.getName,s=i.getVbyList;if(!1===i.isASN1HEX(t))throw"not ASN.1 hex string";try{s(t,0,[1,0],"06"),e=s(t,0,[1,1],"06"),r=s(t,0,[2,0,1],"04");try{n=s(t,0,[2,0,2,0],"03").substr(2)}catch(t){}}catch(t){throw"malformed PKCS#8 plain ECC private key"}if(this.curveName=o(e),void 0===this.curveName)throw"unsupported curve name";this.setNamedCurve(this.curveName),this.setPublicKeyHex(n),this.setPrivateKeyHex(r),this.isPublic=!1},this.readPKCS8PubKeyHex=function(t){var e,r,n=kr,i=Er.crypto.ECDSA.getName,o=n.getVbyList;if(!1===n.isASN1HEX(t))throw"not ASN.1 hex string";try{o(t,0,[0,0],"06"),e=o(t,0,[0,1],"06"),r=o(t,0,[1],"03").substr(2)}catch(t){throw"malformed PKCS#8 ECC public key"}if(this.curveName=i(e),null===this.curveName)throw"unsupported curve name";this.setNamedCurve(this.curveName),this.setPublicKeyHex(r)},this.readCertPubKeyHex=function(t,e){5!==e&&(e=6);var r,n,i=kr,o=Er.crypto.ECDSA.getName,s=i.getVbyList;if(!1===i.isASN1HEX(t))throw"not ASN.1 hex string";try{r=s(t,0,[0,e,0,1],"06"),n=s(t,0,[0,e,1],"03").substr(2)}catch(t){throw"malformed X.509 certificate ECC public key"}if(this.curveName=o(r),null===this.curveName)throw"unsupported curve name";this.setNamedCurve(this.curveName),this.setPublicKeyHex(n)},void 0!==t&&void 0!==t.curve&&(this.curveName=t.curve),void 0===this.curveName&&(this.curveName="secp256r1"),this.setNamedCurve(this.curveName),void 0!==t&&(void 0!==t.prv&&this.setPrivateKeyHex(t.prv),void 0!==t.pub&&this.setPublicKeyHex(t.pub))},Er.crypto.ECDSA.parseSigHex=function(t){var e=Er.crypto.ECDSA.parseSigHexInHexRS(t);return{r:new E(e.r,16),s:new E(e.s,16)}},Er.crypto.ECDSA.parseSigHexInHexRS=function(t){var e=kr,r=e.getChildIdx,n=e.getV;if("30"!=t.substr(0,2))throw"signature is not a ASN.1 sequence";var i=r(t,0);if(2!=i.length)throw"number of signature ASN.1 sequence elements seem wrong";var o=i[0],s=i[1];if("02"!=t.substr(o,2))throw"1st item of sequene of signature is not ASN.1 integer";if("02"!=t.substr(s,2))throw"2nd item of sequene of signature is not ASN.1 integer";return{r:n(t,o),s:n(t,s)}},Er.crypto.ECDSA.asn1SigToConcatSig=function(t){var e=Er.crypto.ECDSA.parseSigHexInHexRS(t),r=e.r,n=e.s;if("00"==r.substr(0,2)&&r.length%32==2&&(r=r.substr(2)),"00"==n.substr(0,2)&&n.length%32==2&&(n=n.substr(2)),r.length%32==30&&(r="00"+r),n.length%32==30&&(n="00"+n),r.length%32!=0)throw"unknown ECDSA sig r length error";if(n.length%32!=0)throw"unknown ECDSA sig s length error";return r+n},Er.crypto.ECDSA.concatSigToASN1Sig=function(t){if(t.length/2*8%128!=0)throw"unknown ECDSA concatinated r-s sig length error";var e=t.substr(0,t.length/2),r=t.substr(t.length/2);return Er.crypto.ECDSA.hexRSSigToASN1Sig(e,r)},Er.crypto.ECDSA.hexRSSigToASN1Sig=function(t,e){var r=new E(t,16),n=new E(e,16);return Er.crypto.ECDSA.biRSSigToASN1Sig(r,n)},Er.crypto.ECDSA.biRSSigToASN1Sig=function(t,e){var r=Er.asn1,n=new r.DERInteger({bigint:t}),i=new r.DERInteger({bigint:e});return new r.DERSequence({array:[n,i]}).getEncodedHex()},Er.crypto.ECDSA.getName=function(t){return"2a8648ce3d030107"===t?"secp256r1":"2b8104000a"===t?"secp256k1":"2b81040022"===t?"secp384r1":-1!=="|secp256r1|NIST P-256|P-256|prime256v1|".indexOf(t)?"secp256r1":-1!=="|secp256k1|".indexOf(t)?"secp256k1":-1!=="|secp384r1|NIST P-384|P-384|".indexOf(t)?"secp384r1":null},void 0!==Er&&Er||(e.KJUR=Er={}),void 0!==Er.crypto&&Er.crypto||(Er.crypto={}),Er.crypto.ECParameterDB=new function(){var t={},e={};function r(t){return new E(t,16)}this.getByName=function(r){var n=r;if(void 0!==e[n]&&(n=e[r]),void 0!==t[n])return t[n];throw"unregistered EC curve name: "+n},this.regist=function(n,i,o,s,a,u,c,f,h,l,p,d){t[n]={};var g=r(o),v=r(s),y=r(a),m=r(u),_=r(c),S=new ze(g,v,y),b=S.decodePointHex("04"+f+h);t[n].name=n,t[n].keylen=i,t[n].curve=S,t[n].G=b,t[n].n=m,t[n].h=_,t[n].oid=p,t[n].info=d;for(var w=0;w=2*u)break}var l={};return l.keyhex=c.substr(0,2*i[e].keylen),l.ivhex=c.substr(2*i[e].keylen,2*i[e].ivlen),l},a=function t(e,r,n,o){var s=y.enc.Base64.parse(e),a=y.enc.Hex.stringify(s);return(0,i[r].proc)(a,n,o)};return{version:"1.0.0",parsePKCS5PEM:function t(e){return o(e)},getKeyAndUnusedIvByPasscodeAndIvsalt:function t(e,r,n){return s(e,r,n)},decryptKeyB64:function t(e,r,n,i){return a(e,r,n,i)},getDecryptedKeyHex:function t(e,r){var n=o(e),i=(n.type,n.cipher),u=n.ivsalt,c=n.data,f=s(i,r,u).keyhex;return a(c,i,f,u)},getEncryptedPKCS5PEMFromPrvKeyHex:function t(e,r,n,o,a){var u="";if(void 0!==o&&null!=o||(o="AES-256-CBC"),void 0===i[o])throw"KEYUTIL unsupported algorithm: "+o;void 0!==a&&null!=a||(a=function t(e){var r=y.lib.WordArray.random(e);return y.enc.Hex.stringify(r)}(i[o].ivlen).toUpperCase());var c=function t(e,r,n,o){return(0,i[r].eproc)(e,n,o)}(r,o,s(o,n,a).keyhex,a);u="-----BEGIN "+e+" PRIVATE KEY-----\r\n";return u+="Proc-Type: 4,ENCRYPTED\r\n",u+="DEK-Info: "+o+","+a+"\r\n",u+="\r\n",u+=c.replace(/(.{64})/g,"$1\r\n"),u+="\r\n-----END "+e+" PRIVATE KEY-----\r\n"},parseHexOfEncryptedPKCS8:function t(e){var r=kr,n=r.getChildIdx,i=r.getV,o={},s=n(e,0);if(2!=s.length)throw"malformed format: SEQUENCE(0).items != 2: "+s.length;o.ciphertext=i(e,s[1]);var a=n(e,s[0]);if(2!=a.length)throw"malformed format: SEQUENCE(0.0).items != 2: "+a.length;if("2a864886f70d01050d"!=i(e,a[0]))throw"this only supports pkcs5PBES2";var u=n(e,a[1]);if(2!=a.length)throw"malformed format: SEQUENCE(0.0.1).items != 2: "+u.length;var c=n(e,u[1]);if(2!=c.length)throw"malformed format: SEQUENCE(0.0.1.1).items != 2: "+c.length;if("2a864886f70d0307"!=i(e,c[0]))throw"this only supports TripleDES";o.encryptionSchemeAlg="TripleDES",o.encryptionSchemeIV=i(e,c[1]);var f=n(e,u[0]);if(2!=f.length)throw"malformed format: SEQUENCE(0.0.1.0).items != 2: "+f.length;if("2a864886f70d01050c"!=i(e,f[0]))throw"this only supports pkcs5PBKDF2";var h=n(e,f[1]);if(h.length<2)throw"malformed format: SEQUENCE(0.0.1.0.1).items < 2: "+h.length;o.pbkdf2Salt=i(e,h[0]);var l=i(e,h[1]);try{o.pbkdf2Iter=parseInt(l,16)}catch(t){throw"malformed format pbkdf2Iter: "+l}return o},getPBKDF2KeyHexFromParam:function t(e,r){var n=y.enc.Hex.parse(e.pbkdf2Salt),i=e.pbkdf2Iter,o=y.PBKDF2(r,n,{keySize:6,iterations:i});return y.enc.Hex.stringify(o)},_getPlainPKCS8HexFromEncryptedPKCS8PEM:function t(e,r){var n=qr(e,"ENCRYPTED PRIVATE KEY"),i=this.parseHexOfEncryptedPKCS8(n),o=tn.getPBKDF2KeyHexFromParam(i,r),s={};s.ciphertext=y.enc.Hex.parse(i.ciphertext);var a=y.enc.Hex.parse(o),u=y.enc.Hex.parse(i.encryptionSchemeIV),c=y.TripleDES.decrypt(s,a,{iv:u});return y.enc.Hex.stringify(c)},getKeyFromEncryptedPKCS8PEM:function t(e,r){var n=this._getPlainPKCS8HexFromEncryptedPKCS8PEM(e,r);return this.getKeyFromPlainPrivatePKCS8Hex(n)},parsePlainPrivatePKCS8Hex:function t(e){var r=kr,n=r.getChildIdx,i=r.getV,o={algparam:null};if("30"!=e.substr(0,2))throw"malformed plain PKCS8 private key(code:001)";var s=n(e,0);if(3!=s.length)throw"malformed plain PKCS8 private key(code:002)";if("30"!=e.substr(s[1],2))throw"malformed PKCS8 private key(code:003)";var a=n(e,s[1]);if(2!=a.length)throw"malformed PKCS8 private key(code:004)";if("06"!=e.substr(a[0],2))throw"malformed PKCS8 private key(code:005)";if(o.algoid=i(e,a[0]),"06"==e.substr(a[1],2)&&(o.algparam=i(e,a[1])),"04"!=e.substr(s[2],2))throw"malformed PKCS8 private key(code:006)";return o.keyidx=r.getVidx(e,s[2]),o},getKeyFromPlainPrivatePKCS8PEM:function t(e){var r=qr(e,"PRIVATE KEY");return this.getKeyFromPlainPrivatePKCS8Hex(r)},getKeyFromPlainPrivatePKCS8Hex:function t(e){var r,n=this.parsePlainPrivatePKCS8Hex(e);if("2a864886f70d010101"==n.algoid)r=new qe;else if("2a8648ce380401"==n.algoid)r=new Er.crypto.DSA;else{if("2a8648ce3d0201"!=n.algoid)throw"unsupported private key algorithm";r=new Er.crypto.ECDSA}return r.readPKCS8PrvKeyHex(e),r},_getKeyFromPublicPKCS8Hex:function t(e){var r,n=kr.getVbyList(e,0,[0,0],"06");if("2a864886f70d010101"===n)r=new qe;else if("2a8648ce380401"===n)r=new Er.crypto.DSA;else{if("2a8648ce3d0201"!==n)throw"unsupported PKCS#8 public key hex";r=new Er.crypto.ECDSA}return r.readPKCS8PubKeyHex(e),r},parsePublicRawRSAKeyHex:function t(e){var r=kr,n=r.getChildIdx,i=r.getV,o={};if("30"!=e.substr(0,2))throw"malformed RSA key(code:001)";var s=n(e,0);if(2!=s.length)throw"malformed RSA key(code:002)";if("02"!=e.substr(s[0],2))throw"malformed RSA key(code:003)";if(o.n=i(e,s[0]),"02"!=e.substr(s[1],2))throw"malformed RSA key(code:004)";return o.e=i(e,s[1]),o},parsePublicPKCS8Hex:function t(e){var r=kr,n=r.getChildIdx,i=r.getV,o={algparam:null},s=n(e,0);if(2!=s.length)throw"outer DERSequence shall have 2 elements: "+s.length;var a=s[0];if("30"!=e.substr(a,2))throw"malformed PKCS8 public key(code:001)";var u=n(e,a);if(2!=u.length)throw"malformed PKCS8 public key(code:002)";if("06"!=e.substr(u[0],2))throw"malformed PKCS8 public key(code:003)";if(o.algoid=i(e,u[0]),"06"==e.substr(u[1],2)?o.algparam=i(e,u[1]):"30"==e.substr(u[1],2)&&(o.algparam={},o.algparam.p=r.getVbyList(e,u[1],[0],"02"),o.algparam.q=r.getVbyList(e,u[1],[1],"02"),o.algparam.g=r.getVbyList(e,u[1],[2],"02")),"03"!=e.substr(s[1],2))throw"malformed PKCS8 public key(code:004)";return o.key=i(e,s[1]).substr(2),o}}}();tn.getKey=function(t,e,r){var n=(v=kr).getChildIdx,i=(v.getV,v.getVbyList),o=Er.crypto,s=o.ECDSA,a=o.DSA,u=qe,c=qr,f=tn;if(void 0!==u&&t instanceof u)return t;if(void 0!==s&&t instanceof s)return t;if(void 0!==a&&t instanceof a)return t;if(void 0!==t.curve&&void 0!==t.xy&&void 0===t.d)return new s({pub:t.xy,curve:t.curve});if(void 0!==t.curve&&void 0!==t.d)return new s({prv:t.d,curve:t.curve});if(void 0===t.kty&&void 0!==t.n&&void 0!==t.e&&void 0===t.d)return(P=new u).setPublic(t.n,t.e),P;if(void 0===t.kty&&void 0!==t.n&&void 0!==t.e&&void 0!==t.d&&void 0!==t.p&&void 0!==t.q&&void 0!==t.dp&&void 0!==t.dq&&void 0!==t.co&&void 0===t.qi)return(P=new u).setPrivateEx(t.n,t.e,t.d,t.p,t.q,t.dp,t.dq,t.co),P;if(void 0===t.kty&&void 0!==t.n&&void 0!==t.e&&void 0!==t.d&&void 0===t.p)return(P=new u).setPrivate(t.n,t.e,t.d),P;if(void 0!==t.p&&void 0!==t.q&&void 0!==t.g&&void 0!==t.y&&void 0===t.x)return(P=new a).setPublic(t.p,t.q,t.g,t.y),P;if(void 0!==t.p&&void 0!==t.q&&void 0!==t.g&&void 0!==t.y&&void 0!==t.x)return(P=new a).setPrivate(t.p,t.q,t.g,t.y,t.x),P;if("RSA"===t.kty&&void 0!==t.n&&void 0!==t.e&&void 0===t.d)return(P=new u).setPublic(Nr(t.n),Nr(t.e)),P;if("RSA"===t.kty&&void 0!==t.n&&void 0!==t.e&&void 0!==t.d&&void 0!==t.p&&void 0!==t.q&&void 0!==t.dp&&void 0!==t.dq&&void 0!==t.qi)return(P=new u).setPrivateEx(Nr(t.n),Nr(t.e),Nr(t.d),Nr(t.p),Nr(t.q),Nr(t.dp),Nr(t.dq),Nr(t.qi)),P;if("RSA"===t.kty&&void 0!==t.n&&void 0!==t.e&&void 0!==t.d)return(P=new u).setPrivate(Nr(t.n),Nr(t.e),Nr(t.d)),P;if("EC"===t.kty&&void 0!==t.crv&&void 0!==t.x&&void 0!==t.y&&void 0===t.d){var h=(k=new s({curve:t.crv})).ecparams.keylen/4,l="04"+("0000000000"+Nr(t.x)).slice(-h)+("0000000000"+Nr(t.y)).slice(-h);return k.setPublicKeyHex(l),k}if("EC"===t.kty&&void 0!==t.crv&&void 0!==t.x&&void 0!==t.y&&void 0!==t.d){h=(k=new s({curve:t.crv})).ecparams.keylen/4,l="04"+("0000000000"+Nr(t.x)).slice(-h)+("0000000000"+Nr(t.y)).slice(-h);var p=("0000000000"+Nr(t.d)).slice(-h);return k.setPublicKeyHex(l),k.setPrivateKeyHex(p),k}if("pkcs5prv"===r){var d,g=t,v=kr;if(9===(d=n(g,0)).length)(P=new u).readPKCS5PrvKeyHex(g);else if(6===d.length)(P=new a).readPKCS5PrvKeyHex(g);else{if(!(d.length>2&&"04"===g.substr(d[1],2)))throw"unsupported PKCS#1/5 hexadecimal key";(P=new s).readPKCS5PrvKeyHex(g)}return P}if("pkcs8prv"===r)return P=f.getKeyFromPlainPrivatePKCS8Hex(t);if("pkcs8pub"===r)return f._getKeyFromPublicPKCS8Hex(t);if("x509pub"===r)return sn.getPublicKeyFromCertHex(t);if(-1!=t.indexOf("-END CERTIFICATE-",0)||-1!=t.indexOf("-END X509 CERTIFICATE-",0)||-1!=t.indexOf("-END TRUSTED CERTIFICATE-",0))return sn.getPublicKeyFromCertPEM(t);if(-1!=t.indexOf("-END PUBLIC KEY-")){var y=qr(t,"PUBLIC KEY");return f._getKeyFromPublicPKCS8Hex(y)}if(-1!=t.indexOf("-END RSA PRIVATE KEY-")&&-1==t.indexOf("4,ENCRYPTED")){var m=c(t,"RSA PRIVATE KEY");return f.getKey(m,null,"pkcs5prv")}if(-1!=t.indexOf("-END DSA PRIVATE KEY-")&&-1==t.indexOf("4,ENCRYPTED")){var _=i(R=c(t,"DSA PRIVATE KEY"),0,[1],"02"),S=i(R,0,[2],"02"),b=i(R,0,[3],"02"),w=i(R,0,[4],"02"),F=i(R,0,[5],"02");return(P=new a).setPrivate(new E(_,16),new E(S,16),new E(b,16),new E(w,16),new E(F,16)),P}if(-1!=t.indexOf("-END PRIVATE KEY-"))return f.getKeyFromPlainPrivatePKCS8PEM(t);if(-1!=t.indexOf("-END RSA PRIVATE KEY-")&&-1!=t.indexOf("4,ENCRYPTED")){var x=f.getDecryptedKeyHex(t,e),A=new qe;return A.readPKCS5PrvKeyHex(x),A}if(-1!=t.indexOf("-END EC PRIVATE KEY-")&&-1!=t.indexOf("4,ENCRYPTED")){var k,P=i(R=f.getDecryptedKeyHex(t,e),0,[1],"04"),C=i(R,0,[2,0],"06"),T=i(R,0,[3,0],"03").substr(2);if(void 0===Er.crypto.OID.oidhex2name[C])throw"undefined OID(hex) in KJUR.crypto.OID: "+C;return(k=new s({curve:Er.crypto.OID.oidhex2name[C]})).setPublicKeyHex(T),k.setPrivateKeyHex(P),k.isPublic=!1,k}if(-1!=t.indexOf("-END DSA PRIVATE KEY-")&&-1!=t.indexOf("4,ENCRYPTED")){var R;_=i(R=f.getDecryptedKeyHex(t,e),0,[1],"02"),S=i(R,0,[2],"02"),b=i(R,0,[3],"02"),w=i(R,0,[4],"02"),F=i(R,0,[5],"02");return(P=new a).setPrivate(new E(_,16),new E(S,16),new E(b,16),new E(w,16),new E(F,16)),P}if(-1!=t.indexOf("-END ENCRYPTED PRIVATE KEY-"))return f.getKeyFromEncryptedPKCS8PEM(t,e);throw"not supported argument"},tn.generateKeypair=function(t,e){if("RSA"==t){var r=e;(s=new qe).generate(r,"10001"),s.isPrivate=!0,s.isPublic=!0;var n=new qe,i=s.n.toString(16),o=s.e.toString(16);return n.setPublic(i,o),n.isPrivate=!1,n.isPublic=!0,(a={}).prvKeyObj=s,a.pubKeyObj=n,a}if("EC"==t){var s,a,u=e,c=new Er.crypto.ECDSA({curve:u}).generateKeyPairHex();return(s=new Er.crypto.ECDSA({curve:u})).setPublicKeyHex(c.ecpubhex),s.setPrivateKeyHex(c.ecprvhex),s.isPrivate=!0,s.isPublic=!1,(n=new Er.crypto.ECDSA({curve:u})).setPublicKeyHex(c.ecpubhex),n.isPrivate=!1,n.isPublic=!0,(a={}).prvKeyObj=s,a.pubKeyObj=n,a}throw"unknown algorithm: "+t},tn.getPEM=function(t,e,r,n,i,o){var s=Er,a=s.asn1,u=a.DERObjectIdentifier,c=a.DERInteger,f=a.ASN1Util.newObject,h=a.x509.SubjectPublicKeyInfo,l=s.crypto,p=l.DSA,d=l.ECDSA,g=qe;function v(t){return f({seq:[{int:0},{int:{bigint:t.n}},{int:t.e},{int:{bigint:t.d}},{int:{bigint:t.p}},{int:{bigint:t.q}},{int:{bigint:t.dmp1}},{int:{bigint:t.dmq1}},{int:{bigint:t.coeff}}]})}function m(t){return f({seq:[{int:1},{octstr:{hex:t.prvKeyHex}},{tag:["a0",!0,{oid:{name:t.curveName}}]},{tag:["a1",!0,{bitstr:{hex:"00"+t.pubKeyHex}}]}]})}function _(t){return f({seq:[{int:0},{int:{bigint:t.p}},{int:{bigint:t.q}},{int:{bigint:t.g}},{int:{bigint:t.y}},{int:{bigint:t.x}}]})}if((void 0!==g&&t instanceof g||void 0!==p&&t instanceof p||void 0!==d&&t instanceof d)&&1==t.isPublic&&(void 0===e||"PKCS8PUB"==e))return Kr(F=new h(t).getEncodedHex(),"PUBLIC KEY");if("PKCS1PRV"==e&&void 0!==g&&t instanceof g&&(void 0===r||null==r)&&1==t.isPrivate)return Kr(F=v(t).getEncodedHex(),"RSA PRIVATE KEY");if("PKCS1PRV"==e&&void 0!==d&&t instanceof d&&(void 0===r||null==r)&&1==t.isPrivate){var S=new u({name:t.curveName}).getEncodedHex(),b=m(t).getEncodedHex(),w="";return w+=Kr(S,"EC PARAMETERS"),w+=Kr(b,"EC PRIVATE KEY")}if("PKCS1PRV"==e&&void 0!==p&&t instanceof p&&(void 0===r||null==r)&&1==t.isPrivate)return Kr(F=_(t).getEncodedHex(),"DSA PRIVATE KEY");if("PKCS5PRV"==e&&void 0!==g&&t instanceof g&&void 0!==r&&null!=r&&1==t.isPrivate){var F=v(t).getEncodedHex();return void 0===n&&(n="DES-EDE3-CBC"),this.getEncryptedPKCS5PEMFromPrvKeyHex("RSA",F,r,n,o)}if("PKCS5PRV"==e&&void 0!==d&&t instanceof d&&void 0!==r&&null!=r&&1==t.isPrivate){F=m(t).getEncodedHex();return void 0===n&&(n="DES-EDE3-CBC"),this.getEncryptedPKCS5PEMFromPrvKeyHex("EC",F,r,n,o)}if("PKCS5PRV"==e&&void 0!==p&&t instanceof p&&void 0!==r&&null!=r&&1==t.isPrivate){F=_(t).getEncodedHex();return void 0===n&&(n="DES-EDE3-CBC"),this.getEncryptedPKCS5PEMFromPrvKeyHex("DSA",F,r,n,o)}var E=function t(e,r){var n=x(e,r);return new f({seq:[{seq:[{oid:{name:"pkcs5PBES2"}},{seq:[{seq:[{oid:{name:"pkcs5PBKDF2"}},{seq:[{octstr:{hex:n.pbkdf2Salt}},{int:n.pbkdf2Iter}]}]},{seq:[{oid:{name:"des-EDE3-CBC"}},{octstr:{hex:n.encryptionSchemeIV}}]}]}]},{octstr:{hex:n.ciphertext}}]}).getEncodedHex()},x=function t(e,r){var n=y.lib.WordArray.random(8),i=y.lib.WordArray.random(8),o=y.PBKDF2(r,n,{keySize:6,iterations:100}),s=y.enc.Hex.parse(e),a=y.TripleDES.encrypt(s,o,{iv:i})+"",u={};return u.ciphertext=a,u.pbkdf2Salt=y.enc.Hex.stringify(n),u.pbkdf2Iter=100,u.encryptionSchemeAlg="DES-EDE3-CBC",u.encryptionSchemeIV=y.enc.Hex.stringify(i),u};if("PKCS8PRV"==e&&void 0!=g&&t instanceof g&&1==t.isPrivate){var A=v(t).getEncodedHex();F=f({seq:[{int:0},{seq:[{oid:{name:"rsaEncryption"}},{null:!0}]},{octstr:{hex:A}}]}).getEncodedHex();return void 0===r||null==r?Kr(F,"PRIVATE KEY"):Kr(b=E(F,r),"ENCRYPTED PRIVATE KEY")}if("PKCS8PRV"==e&&void 0!==d&&t instanceof d&&1==t.isPrivate){A=new f({seq:[{int:1},{octstr:{hex:t.prvKeyHex}},{tag:["a1",!0,{bitstr:{hex:"00"+t.pubKeyHex}}]}]}).getEncodedHex(),F=f({seq:[{int:0},{seq:[{oid:{name:"ecPublicKey"}},{oid:{name:t.curveName}}]},{octstr:{hex:A}}]}).getEncodedHex();return void 0===r||null==r?Kr(F,"PRIVATE KEY"):Kr(b=E(F,r),"ENCRYPTED PRIVATE KEY")}if("PKCS8PRV"==e&&void 0!==p&&t instanceof p&&1==t.isPrivate){A=new c({bigint:t.x}).getEncodedHex(),F=f({seq:[{int:0},{seq:[{oid:{name:"dsa"}},{seq:[{int:{bigint:t.p}},{int:{bigint:t.q}},{int:{bigint:t.g}}]}]},{octstr:{hex:A}}]}).getEncodedHex();return void 0===r||null==r?Kr(F,"PRIVATE KEY"):Kr(b=E(F,r),"ENCRYPTED PRIVATE KEY")}throw"unsupported object nor format"},tn.getKeyFromCSRPEM=function(t){var e=qr(t,"CERTIFICATE REQUEST");return tn.getKeyFromCSRHex(e)},tn.getKeyFromCSRHex=function(t){var e=tn.parseCSRHex(t);return tn.getKey(e.p8pubkeyhex,null,"pkcs8pub")},tn.parseCSRHex=function(t){var e=kr,r=e.getChildIdx,n=e.getTLV,i={},o=t;if("30"!=o.substr(0,2))throw"malformed CSR(code:001)";var s=r(o,0);if(s.length<1)throw"malformed CSR(code:002)";if("30"!=o.substr(s[0],2))throw"malformed CSR(code:003)";var a=r(o,s[0]);if(a.length<3)throw"malformed CSR(code:004)";return i.p8pubkeyhex=n(o,a[2]),i},tn.getJWKFromKey=function(t){var e={};if(t instanceof qe&&t.isPrivate)return e.kty="RSA",e.n=Dr(t.n.toString(16)),e.e=Dr(t.e.toString(16)),e.d=Dr(t.d.toString(16)),e.p=Dr(t.p.toString(16)),e.q=Dr(t.q.toString(16)),e.dp=Dr(t.dmp1.toString(16)),e.dq=Dr(t.dmq1.toString(16)),e.qi=Dr(t.coeff.toString(16)),e;if(t instanceof qe&&t.isPublic)return e.kty="RSA",e.n=Dr(t.n.toString(16)),e.e=Dr(t.e.toString(16)),e;if(t instanceof Er.crypto.ECDSA&&t.isPrivate){if("P-256"!==(n=t.getShortNISTPCurveName())&&"P-384"!==n)throw"unsupported curve name for JWT: "+n;var r=t.getPublicKeyXYHex();return e.kty="EC",e.crv=n,e.x=Dr(r.x),e.y=Dr(r.y),e.d=Dr(t.prvKeyHex),e}if(t instanceof Er.crypto.ECDSA&&t.isPublic){var n;if("P-256"!==(n=t.getShortNISTPCurveName())&&"P-384"!==n)throw"unsupported curve name for JWT: "+n;r=t.getPublicKeyXYHex();return e.kty="EC",e.crv=n,e.x=Dr(r.x),e.y=Dr(r.y),e}throw"not supported key object"},qe.getPosArrayOfChildrenFromHex=function(t){return kr.getChildIdx(t,0)},qe.getHexValueArrayOfChildrenFromHex=function(t){var e,r=kr.getV,n=r(t,(e=qe.getPosArrayOfChildrenFromHex(t))[0]),i=r(t,e[1]),o=r(t,e[2]),s=r(t,e[3]),a=r(t,e[4]),u=r(t,e[5]),c=r(t,e[6]),f=r(t,e[7]),h=r(t,e[8]);return(e=new Array).push(n,i,o,s,a,u,c,f,h),e},qe.prototype.readPrivateKeyFromPEMString=function(t){var e=qr(t),r=qe.getHexValueArrayOfChildrenFromHex(e);this.setPrivateEx(r[1],r[2],r[3],r[4],r[5],r[6],r[7],r[8])},qe.prototype.readPKCS5PrvKeyHex=function(t){var e=qe.getHexValueArrayOfChildrenFromHex(t);this.setPrivateEx(e[1],e[2],e[3],e[4],e[5],e[6],e[7],e[8])},qe.prototype.readPKCS8PrvKeyHex=function(t){var e,r,n,i,o,s,a,u,c=kr,f=c.getVbyList;if(!1===c.isASN1HEX(t))throw"not ASN.1 hex string";try{e=f(t,0,[2,0,1],"02"),r=f(t,0,[2,0,2],"02"),n=f(t,0,[2,0,3],"02"),i=f(t,0,[2,0,4],"02"),o=f(t,0,[2,0,5],"02"),s=f(t,0,[2,0,6],"02"),a=f(t,0,[2,0,7],"02"),u=f(t,0,[2,0,8],"02")}catch(t){throw"malformed PKCS#8 plain RSA private key"}this.setPrivateEx(e,r,n,i,o,s,a,u)},qe.prototype.readPKCS5PubKeyHex=function(t){var e=kr,r=e.getV;if(!1===e.isASN1HEX(t))throw"keyHex is not ASN.1 hex string";var n=e.getChildIdx(t,0);if(2!==n.length||"02"!==t.substr(n[0],2)||"02"!==t.substr(n[1],2))throw"wrong hex for PKCS#5 public key";var i=r(t,n[0]),o=r(t,n[1]);this.setPublic(i,o)},qe.prototype.readPKCS8PubKeyHex=function(t){var e=kr;if(!1===e.isASN1HEX(t))throw"not ASN.1 hex string";if("06092a864886f70d010101"!==e.getTLVbyList(t,0,[0,0]))throw"not PKCS8 RSA public key";var r=e.getTLVbyList(t,0,[1,0]);this.readPKCS5PubKeyHex(r)},qe.prototype.readCertPubKeyHex=function(t,e){var r,n;(r=new sn).readCertHex(t),n=r.getPublicKeyHex(),this.readPKCS8PubKeyHex(n)};var en=new RegExp("");function rn(t,e){for(var r="",n=e/4-t.length,i=0;i>24,(16711680&i)>>16,(65280&i)>>8,255&i])))),i+=1;return n}function on(t){for(var e in Er.crypto.Util.DIGESTINFOHEAD){var r=Er.crypto.Util.DIGESTINFOHEAD[e],n=r.length;if(t.substring(0,n)==r)return[e,t.substring(n)]}return[]}function sn(){var t=kr,e=t.getChildIdx,r=t.getV,n=t.getTLV,i=t.getVbyList,o=t.getTLVbyList,s=t.getIdxbyList,a=t.getVidx,u=t.oidname,c=sn,f=qr;this.hex=null,this.version=0,this.foffset=0,this.aExtInfo=null,this.getVersion=function(){return null===this.hex||0!==this.version?this.version:"a003020102"!==o(this.hex,0,[0,0])?(this.version=1,this.foffset=-1,1):(this.version=3,3)},this.getSerialNumberHex=function(){return i(this.hex,0,[0,1+this.foffset],"02")},this.getSignatureAlgorithmField=function(){return u(i(this.hex,0,[0,2+this.foffset,0],"06"))},this.getIssuerHex=function(){return o(this.hex,0,[0,3+this.foffset],"30")},this.getIssuerString=function(){return c.hex2dn(this.getIssuerHex())},this.getSubjectHex=function(){return o(this.hex,0,[0,5+this.foffset],"30")},this.getSubjectString=function(){return c.hex2dn(this.getSubjectHex())},this.getNotBefore=function(){var t=i(this.hex,0,[0,4+this.foffset,0]);return t=t.replace(/(..)/g,"%$1"),t=decodeURIComponent(t)},this.getNotAfter=function(){var t=i(this.hex,0,[0,4+this.foffset,1]);return t=t.replace(/(..)/g,"%$1"),t=decodeURIComponent(t)},this.getPublicKeyHex=function(){return t.getTLVbyList(this.hex,0,[0,6+this.foffset],"30")},this.getPublicKeyIdx=function(){return s(this.hex,0,[0,6+this.foffset],"30")},this.getPublicKeyContentIdx=function(){var t=this.getPublicKeyIdx();return s(this.hex,t,[1,0],"30")},this.getPublicKey=function(){return tn.getKey(this.getPublicKeyHex(),null,"pkcs8pub")},this.getSignatureAlgorithmName=function(){return u(i(this.hex,0,[1,0],"06"))},this.getSignatureValueHex=function(){return i(this.hex,0,[2],"03",!0)},this.verifySignature=function(t){var e=this.getSignatureAlgorithmName(),r=this.getSignatureValueHex(),n=o(this.hex,0,[0],"30"),i=new Er.crypto.Signature({alg:e});return i.init(t),i.updateHex(n),i.verify(r)},this.parseExt=function(){if(3!==this.version)return-1;var r=s(this.hex,0,[0,7,0],"30"),n=e(this.hex,r);this.aExtInfo=new Array;for(var o=0;o0&&(c=new Array(r),(new He).nextBytes(c),c=String.fromCharCode.apply(String,c));var f=jr(u(Ur("\0\0\0\0\0\0\0\0"+i+c))),h=[];for(n=0;n>8*a-s&255;for(d[0]&=~g,n=0;nthis.n.bitLength())return 0;var n=on(this.doPublic(r).toString(16).replace(/^1f+00/,""));if(0==n.length)return!1;var i=n[0];return n[1]==function t(e){return Er.crypto.Util.hashString(e,i)}(t)},qe.prototype.verifyWithMessageHash=function(t,e){var r=Ve(e=(e=e.replace(en,"")).replace(/[ \n]+/g,""),16);if(r.bitLength()>this.n.bitLength())return 0;var n=on(this.doPublic(r).toString(16).replace(/^1f+00/,""));if(0==n.length)return!1;n[0];return n[1]==t},qe.prototype.verifyPSS=function(t,e,r,n){var i=function t(e){return Er.crypto.Util.hashHex(e,r)}(Ur(t));return void 0===n&&(n=-1),this.verifyWithMessageHashPSS(i,e,r,n)},qe.prototype.verifyWithMessageHashPSS=function(t,e,r,n){var i=new E(e,16);if(i.bitLength()>this.n.bitLength())return!1;var o,s=function t(e){return Er.crypto.Util.hashHex(e,r)},a=jr(t),u=a.length,c=this.n.bitLength()-1,f=Math.ceil(c/8);if(-1===n||void 0===n)n=u;else if(-2===n)n=f-u-2;else if(n<-2)throw"invalid salt length";if(f>8*f-c&255;if(0!=(l.charCodeAt(0)&d))throw"bits beyond keysize not zero";var g=nn(p,l.length,s),v=[];for(o=0;o0)&&-1==(":"+n.join(":")+":").indexOf(":"+v+":"))throw"algorithm '"+v+"' not accepted in the list";if("none"!=v&&null===e)throw"key shall be specified to verify.";if("string"==typeof e&&-1!=e.indexOf("-----BEGIN ")&&(e=tn.getKey(e)),!("RS"!=y&&"PS"!=y||e instanceof i))throw"key shall be a RSAKey obj for RS* and PS* algs";if("ES"==y&&!(e instanceof c))throw"key shall be a ECDSA obj for ES* algs";var m=null;if(void 0===s.jwsalg2sigalg[g.alg])throw"unsupported alg name: "+v;if("none"==(m=s.jwsalg2sigalg[v]))throw"not supported";if("Hmac"==m.substr(0,4)){if(void 0===e)throw"hexadecimal key shall be specified for HMAC";var _=new f({alg:m,pass:e});return _.updateString(p),d==_.doFinal()}if(-1!=m.indexOf("withECDSA")){var S,b=null;try{b=c.concatSigToASN1Sig(d)}catch(t){return!1}return(S=new h({alg:m})).init(e),S.updateString(p),S.verify(b)}return(S=new h({alg:m})).init(e),S.updateString(p),S.verify(d)},Er.jws.JWS.parse=function(t){var e,r,n,i=t.split("."),o={};if(2!=i.length&&3!=i.length)throw"malformed sJWS: wrong number of '.' splitted elements";return e=i[0],r=i[1],3==i.length&&(n=i[2]),o.headerObj=Er.jws.JWS.readSafeJSONString(Ar(e)),o.payloadObj=Er.jws.JWS.readSafeJSONString(Ar(r)),o.headerPP=JSON.stringify(o.headerObj,null," "),null==o.payloadObj?o.payloadPP=Ar(r):o.payloadPP=JSON.stringify(o.payloadObj,null," "),void 0!==n&&(o.sigHex=Nr(n)),o},Er.jws.JWS.verifyJWT=function(t,e,n){var i=Er.jws,o=i.JWS,s=o.readSafeJSONString,a=o.inArray,u=o.includedArray,c=t.split("."),f=c[0],h=c[1],l=(Nr(c[2]),s(Ar(f))),p=s(Ar(h));if(void 0===l.alg)return!1;if(void 0===n.alg)throw"acceptField.alg shall be specified";if(!a(l.alg,n.alg))return!1;if(void 0!==p.iss&&"object"===r(n.iss)&&!a(p.iss,n.iss))return!1;if(void 0!==p.sub&&"object"===r(n.sub)&&!a(p.sub,n.sub))return!1;if(void 0!==p.aud&&"object"===r(n.aud))if("string"==typeof p.aud){if(!a(p.aud,n.aud))return!1}else if("object"==r(p.aud)&&!u(p.aud,n.aud))return!1;var d=i.IntDate.getNow();return void 0!==n.verifyAt&&"number"==typeof n.verifyAt&&(d=n.verifyAt),void 0!==n.gracePeriod&&"number"==typeof n.gracePeriod||(n.gracePeriod=0),!(void 0!==p.exp&&"number"==typeof p.exp&&p.exp+n.gracePeriodr.length&&(n=r.length);for(var i=0;i + * @license MIT + */ +var n=r(361),i=r(362),o=r(363);function s(){return u.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function a(t,e){if(s()=s())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+s().toString(16)+" bytes");return 0|t}function d(t,e){if(u.isBuffer(t))return t.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(t)||t instanceof ArrayBuffer))return t.byteLength;"string"!=typeof t&&(t=""+t);var r=t.length;if(0===r)return 0;for(var n=!1;;)switch(e){case"ascii":case"latin1":case"binary":return r;case"utf8":case"utf-8":case void 0:return V(t).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*r;case"hex":return r>>>1;case"base64":return K(t).length;default:if(n)return V(t).length;e=(""+e).toLowerCase(),n=!0}}function g(t,e,r){var n=t[e];t[e]=t[r],t[r]=n}function v(t,e,r,n,i){if(0===t.length)return-1;if("string"==typeof r?(n=r,r=0):r>2147483647?r=2147483647:r<-2147483648&&(r=-2147483648),r=+r,isNaN(r)&&(r=i?0:t.length-1),r<0&&(r=t.length+r),r>=t.length){if(i)return-1;r=t.length-1}else if(r<0){if(!i)return-1;r=0}if("string"==typeof e&&(e=u.from(e,n)),u.isBuffer(e))return 0===e.length?-1:y(t,e,r,n,i);if("number"==typeof e)return e&=255,u.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?i?Uint8Array.prototype.indexOf.call(t,e,r):Uint8Array.prototype.lastIndexOf.call(t,e,r):y(t,[e],r,n,i);throw new TypeError("val must be string, number or Buffer")}function y(t,e,r,n,i){var o,s=1,a=t.length,u=e.length;if(void 0!==n&&("ucs2"===(n=String(n).toLowerCase())||"ucs-2"===n||"utf16le"===n||"utf-16le"===n)){if(t.length<2||e.length<2)return-1;s=2,a/=2,u/=2,r/=2}function c(t,e){return 1===s?t[e]:t.readUInt16BE(e*s)}if(i){var f=-1;for(o=r;oa&&(r=a-u),o=r;o>=0;o--){for(var h=!0,l=0;li&&(n=i):n=i;var o=e.length;if(o%2!=0)throw new TypeError("Invalid hex string");n>o/2&&(n=o/2);for(var s=0;s>8,i=r%256,o.push(i),o.push(n);return o}(e,t.length-r),t,r,n)}function E(t,e,r){return 0===e&&r===t.length?n.fromByteArray(t):n.fromByteArray(t.slice(e,r))}function x(t,e,r){r=Math.min(t.length,r);for(var n=[],i=e;i239?4:c>223?3:c>191?2:1;if(i+h<=r)switch(h){case 1:c<128&&(f=c);break;case 2:128==(192&(o=t[i+1]))&&(u=(31&c)<<6|63&o)>127&&(f=u);break;case 3:o=t[i+1],s=t[i+2],128==(192&o)&&128==(192&s)&&(u=(15&c)<<12|(63&o)<<6|63&s)>2047&&(u<55296||u>57343)&&(f=u);break;case 4:o=t[i+1],s=t[i+2],a=t[i+3],128==(192&o)&&128==(192&s)&&128==(192&a)&&(u=(15&c)<<18|(63&o)<<12|(63&s)<<6|63&a)>65535&&u<1114112&&(f=u)}null===f?(f=65533,h=1):f>65535&&(f-=65536,n.push(f>>>10&1023|55296),f=56320|1023&f),n.push(f),i+=h}return function l(t){var e=t.length;if(e<=P)return String.fromCharCode.apply(String,t);var r="",n=0;for(;nthis.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(e>>>=0))return"";for(t||(t="utf8");;)switch(t){case"hex":return R(this,e,n);case"utf8":case"utf-8":return x(this,e,n);case"ascii":return C(this,e,n);case"latin1":case"binary":return T(this,e,n);case"base64":return E(this,e,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return I(this,e,n);default:if(i)throw new TypeError("Unknown encoding: "+t);t=(t+"").toLowerCase(),i=!0}}.apply(this,arguments)},u.prototype.equals=function t(e){if(!u.isBuffer(e))throw new TypeError("Argument must be a Buffer");return this===e||0===u.compare(this,e)},u.prototype.inspect=function t(){var r="",n=e.INSPECT_MAX_BYTES;return this.length>0&&(r=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(r+=" ... ")),""},u.prototype.compare=function t(e,r,n,i,o){if(!u.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===r&&(r=0),void 0===n&&(n=e?e.length:0),void 0===i&&(i=0),void 0===o&&(o=this.length),r<0||n>e.length||i<0||o>this.length)throw new RangeError("out of range index");if(i>=o&&r>=n)return 0;if(i>=o)return-1;if(r>=n)return 1;if(r>>>=0,n>>>=0,i>>>=0,o>>>=0,this===e)return 0;for(var s=o-i,a=n-r,c=Math.min(s,a),f=this.slice(i,o),h=e.slice(r,n),l=0;lo)&&(n=o),e.length>0&&(n<0||r<0)||r>this.length)throw new RangeError("Attempt to write outside buffer bounds");i||(i="utf8");for(var s=!1;;)switch(i){case"hex":return m(this,e,r,n);case"utf8":case"utf-8":return _(this,e,r,n);case"ascii":return S(this,e,r,n);case"latin1":case"binary":return b(this,e,r,n);case"base64":return w(this,e,r,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return F(this,e,r,n);default:if(s)throw new TypeError("Unknown encoding: "+i);i=(""+i).toLowerCase(),s=!0}},u.prototype.toJSON=function t(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var P=4096;function C(t,e,r){var n="";r=Math.min(t.length,r);for(var i=e;in)&&(r=n);for(var i="",o=e;or)throw new RangeError("Trying to access beyond buffer length")}function D(t,e,r,n,i,o){if(!u.isBuffer(t))throw new TypeError('"buffer" argument must be a Buffer instance');if(e>i||et.length)throw new RangeError("Index out of range")}function N(t,e,r,n){e<0&&(e=65535+e+1);for(var i=0,o=Math.min(t.length-r,2);i>>8*(n?i:1-i)}function L(t,e,r,n){e<0&&(e=4294967295+e+1);for(var i=0,o=Math.min(t.length-r,4);i>>8*(n?i:3-i)&255}function M(t,e,r,n,i,o){if(r+n>t.length)throw new RangeError("Index out of range");if(r<0)throw new RangeError("Index out of range")}function j(t,e,r,n,o){return o||M(t,0,r,4),i.write(t,e,r,n,23,4),r+4}function U(t,e,r,n,o){return o||M(t,0,r,8),i.write(t,e,r,n,52,8),r+8}u.prototype.slice=function t(e,r){var n,i=this.length;if(e=~~e,r=void 0===r?i:~~r,e<0?(e+=i)<0&&(e=0):e>i&&(e=i),r<0?(r+=i)<0&&(r=0):r>i&&(r=i),r0&&(o*=256);)i+=this[e+--r]*o;return i},u.prototype.readUInt8=function t(e,r){return r||O(e,1,this.length),this[e]},u.prototype.readUInt16LE=function t(e,r){return r||O(e,2,this.length),this[e]|this[e+1]<<8},u.prototype.readUInt16BE=function t(e,r){return r||O(e,2,this.length),this[e]<<8|this[e+1]},u.prototype.readUInt32LE=function t(e,r){return r||O(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},u.prototype.readUInt32BE=function t(e,r){return r||O(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},u.prototype.readIntLE=function t(e,r,n){e|=0,r|=0,n||O(e,r,this.length);for(var i=this[e],o=1,s=0;++s=(o*=128)&&(i-=Math.pow(2,8*r)),i},u.prototype.readIntBE=function t(e,r,n){e|=0,r|=0,n||O(e,r,this.length);for(var i=r,o=1,s=this[e+--i];i>0&&(o*=256);)s+=this[e+--i]*o;return s>=(o*=128)&&(s-=Math.pow(2,8*r)),s},u.prototype.readInt8=function t(e,r){return r||O(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},u.prototype.readInt16LE=function t(e,r){r||O(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt16BE=function t(e,r){r||O(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt32LE=function t(e,r){return r||O(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},u.prototype.readInt32BE=function t(e,r){return r||O(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},u.prototype.readFloatLE=function t(e,r){return r||O(e,4,this.length),i.read(this,e,!0,23,4)},u.prototype.readFloatBE=function t(e,r){return r||O(e,4,this.length),i.read(this,e,!1,23,4)},u.prototype.readDoubleLE=function t(e,r){return r||O(e,8,this.length),i.read(this,e,!0,52,8)},u.prototype.readDoubleBE=function t(e,r){return r||O(e,8,this.length),i.read(this,e,!1,52,8)},u.prototype.writeUIntLE=function t(e,r,n,i){(e=+e,r|=0,n|=0,i)||D(this,e,r,n,Math.pow(2,8*n)-1,0);var o=1,s=0;for(this[r]=255&e;++s=0&&(s*=256);)this[r+o]=e/s&255;return r+n},u.prototype.writeUInt8=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,1,255,0),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[r]=255&e,r+1},u.prototype.writeUInt16LE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[r]=255&e,this[r+1]=e>>>8):N(this,e,r,!0),r+2},u.prototype.writeUInt16BE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[r]=e>>>8,this[r+1]=255&e):N(this,e,r,!1),r+2},u.prototype.writeUInt32LE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[r+3]=e>>>24,this[r+2]=e>>>16,this[r+1]=e>>>8,this[r]=255&e):L(this,e,r,!0),r+4},u.prototype.writeUInt32BE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[r]=e>>>24,this[r+1]=e>>>16,this[r+2]=e>>>8,this[r+3]=255&e):L(this,e,r,!1),r+4},u.prototype.writeIntLE=function t(e,r,n,i){if(e=+e,r|=0,!i){var o=Math.pow(2,8*n-1);D(this,e,r,n,o-1,-o)}var s=0,a=1,u=0;for(this[r]=255&e;++s>0)-u&255;return r+n},u.prototype.writeIntBE=function t(e,r,n,i){if(e=+e,r|=0,!i){var o=Math.pow(2,8*n-1);D(this,e,r,n,o-1,-o)}var s=n-1,a=1,u=0;for(this[r+s]=255&e;--s>=0&&(a*=256);)e<0&&0===u&&0!==this[r+s+1]&&(u=1),this[r+s]=(e/a>>0)-u&255;return r+n},u.prototype.writeInt8=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,1,127,-128),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[r]=255&e,r+1},u.prototype.writeInt16LE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[r]=255&e,this[r+1]=e>>>8):N(this,e,r,!0),r+2},u.prototype.writeInt16BE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[r]=e>>>8,this[r+1]=255&e):N(this,e,r,!1),r+2},u.prototype.writeInt32LE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,4,2147483647,-2147483648),u.TYPED_ARRAY_SUPPORT?(this[r]=255&e,this[r+1]=e>>>8,this[r+2]=e>>>16,this[r+3]=e>>>24):L(this,e,r,!0),r+4},u.prototype.writeInt32BE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),u.TYPED_ARRAY_SUPPORT?(this[r]=e>>>24,this[r+1]=e>>>16,this[r+2]=e>>>8,this[r+3]=255&e):L(this,e,r,!1),r+4},u.prototype.writeFloatLE=function t(e,r,n){return j(this,e,r,!0,n)},u.prototype.writeFloatBE=function t(e,r,n){return j(this,e,r,!1,n)},u.prototype.writeDoubleLE=function t(e,r,n){return U(this,e,r,!0,n)},u.prototype.writeDoubleBE=function t(e,r,n){return U(this,e,r,!1,n)},u.prototype.copy=function t(e,r,n,i){if(n||(n=0),i||0===i||(i=this.length),r>=e.length&&(r=e.length),r||(r=0),i>0&&i=this.length)throw new RangeError("sourceStart out of bounds");if(i<0)throw new RangeError("sourceEnd out of bounds");i>this.length&&(i=this.length),e.length-r=0;--o)e[o+r]=this[o+n];else if(s<1e3||!u.TYPED_ARRAY_SUPPORT)for(o=0;o>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(s=r;s55295&&r<57344){if(!i){if(r>56319){(e-=3)>-1&&o.push(239,191,189);continue}if(s+1===n){(e-=3)>-1&&o.push(239,191,189);continue}i=r;continue}if(r<56320){(e-=3)>-1&&o.push(239,191,189),i=r;continue}r=65536+(i-55296<<10|r-56320)}else i&&(e-=3)>-1&&o.push(239,191,189);if(i=null,r<128){if((e-=1)<0)break;o.push(r)}else if(r<2048){if((e-=2)<0)break;o.push(r>>6|192,63&r|128)}else if(r<65536){if((e-=3)<0)break;o.push(r>>12|224,r>>6&63|128,63&r|128)}else{if(!(r<1114112))throw new Error("Invalid code point");if((e-=4)<0)break;o.push(r>>18|240,r>>12&63|128,r>>6&63|128,63&r|128)}}return o}function K(t){return n.toByteArray(function e(t){if((t=function e(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}(t).replace(B,"")).length<2)return"";for(;t.length%4!=0;)t+="=";return t}(t))}function q(t,e,r,n){for(var i=0;i=e.length||i>=t.length);++i)e[i+r]=t[i];return i}}).call(this,r(71))},function(t,e,r){"use strict";e.byteLength=function n(t){var e=l(t),r=e[0],n=e[1];return 3*(r+n)/4-n},e.toByteArray=function i(t){for(var e,r=l(t),n=r[0],i=r[1],o=new u(function s(t,e,r){return 3*(e+r)/4-r}(0,n,i)),c=0,f=i>0?n-4:n,h=0;h>16&255,o[c++]=e>>8&255,o[c++]=255&e;2===i&&(e=a[t.charCodeAt(h)]<<2|a[t.charCodeAt(h+1)]>>4,o[c++]=255&e);1===i&&(e=a[t.charCodeAt(h)]<<10|a[t.charCodeAt(h+1)]<<4|a[t.charCodeAt(h+2)]>>2,o[c++]=e>>8&255,o[c++]=255&e);return o},e.fromByteArray=function o(t){for(var e,r=t.length,n=r%3,i=[],o=0,a=r-n;oa?a:o+16383));1===n?(e=t[r-1],i.push(s[e>>2]+s[e<<4&63]+"==")):2===n&&(e=(t[r-2]<<8)+t[r-1],i.push(s[e>>10]+s[e>>4&63]+s[e<<2&63]+"="));return i.join("")};for(var s=[],a=[],u="undefined"!=typeof Uint8Array?Uint8Array:Array,c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",f=0,h=c.length;f0)throw new Error("Invalid string. Length must be a multiple of 4");var r=t.indexOf("=");return-1===r&&(r=e),[r,r===e?0:4-r%4]}function p(t,e,r){for(var n,i,o=[],a=e;a>18&63]+s[i>>12&63]+s[i>>6&63]+s[63&i]);return o.join("")}a["-".charCodeAt(0)]=62,a["_".charCodeAt(0)]=63},function(t,e){e.read=function(t,e,r,n,i){var o,s,a=8*i-n-1,u=(1<>1,f=-7,h=r?i-1:0,l=r?-1:1,p=t[e+h];for(h+=l,o=p&(1<<-f)-1,p>>=-f,f+=a;f>0;o=256*o+t[e+h],h+=l,f-=8);for(s=o&(1<<-f)-1,o>>=-f,f+=n;f>0;s=256*s+t[e+h],h+=l,f-=8);if(0===o)o=1-c;else{if(o===u)return s?NaN:1/0*(p?-1:1);s+=Math.pow(2,n),o-=c}return(p?-1:1)*s*Math.pow(2,o-n)},e.write=function(t,e,r,n,i,o){var s,a,u,c=8*o-i-1,f=(1<>1,l=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,p=n?0:o-1,d=n?1:-1,g=e<0||0===e&&1/e<0?1:0;for(e=Math.abs(e),isNaN(e)||e===1/0?(a=isNaN(e)?1:0,s=f):(s=Math.floor(Math.log(e)/Math.LN2),e*(u=Math.pow(2,-s))<1&&(s--,u*=2),(e+=s+h>=1?l/u:l*Math.pow(2,1-h))*u>=2&&(s++,u/=2),s+h>=f?(a=0,s=f):s+h>=1?(a=(e*u-1)*Math.pow(2,i),s+=h):(a=e*Math.pow(2,h-1)*Math.pow(2,i),s=0));i>=8;t[r+p]=255&a,p+=d,a/=256,i-=8);for(s=s<0;t[r+p]=255&s,p+=d,s/=256,c-=8);t[r+p-d]|=128*g}},function(t,e){var r={}.toString;t.exports=Array.isArray||function(t){return"[object Array]"==r.call(t)}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.default=function n(t){var e=t.jws,r=t.KeyUtil,n=t.X509,o=t.crypto,s=t.hextob64u,a=t.b64tohex,u=t.AllowedSigningAlgs;return function(){function t(){!function e(t,r){if(!(t instanceof r))throw new TypeError("Cannot call a class as a function")}(this,t)}return t.parseJwt=function t(r){i.Log.debug("JoseUtil.parseJwt");try{var n=e.JWS.parse(r);return{header:n.headerObj,payload:n.payloadObj}}catch(t){i.Log.error(t)}},t.validateJwt=function e(o,s,u,c,f,h,l){i.Log.debug("JoseUtil.validateJwt");try{if("RSA"===s.kty)if(s.e&&s.n)s=r.getKey(s);else{if(!s.x5c||!s.x5c.length)return i.Log.error("JoseUtil.validateJwt: RSA key missing key material",s),Promise.reject(new Error("RSA key missing key material"));var p=a(s.x5c[0]);s=n.getPublicKeyFromCertHex(p)}else{if("EC"!==s.kty)return i.Log.error("JoseUtil.validateJwt: Unsupported key type",s&&s.kty),Promise.reject(new Error(s.kty));if(!(s.crv&&s.x&&s.y))return i.Log.error("JoseUtil.validateJwt: EC key missing key material",s),Promise.reject(new Error("EC key missing key material"));s=r.getKey(s)}return t._validateJwt(o,s,u,c,f,h,l)}catch(t){return i.Log.error(t&&t.message||t),Promise.reject("JWT validation failed")}},t.validateJwtAttributes=function e(r,n,o,s,a,u){s||(s=0),a||(a=parseInt(Date.now()/1e3));var c=t.parseJwt(r).payload;if(!c.iss)return i.Log.error("JoseUtil._validateJwt: issuer was not provided"),Promise.reject(new Error("issuer was not provided"));if(c.iss!==n)return i.Log.error("JoseUtil._validateJwt: Invalid issuer in token",c.iss),Promise.reject(new Error("Invalid issuer in token: "+c.iss));if(!c.aud)return i.Log.error("JoseUtil._validateJwt: aud was not provided"),Promise.reject(new Error("aud was not provided"));var f=c.aud===o||Array.isArray(c.aud)&&c.aud.indexOf(o)>=0;if(!f)return i.Log.error("JoseUtil._validateJwt: Invalid audience in token",c.aud),Promise.reject(new Error("Invalid audience in token: "+c.aud));if(c.azp&&c.azp!==o)return i.Log.error("JoseUtil._validateJwt: Invalid azp in token",c.azp),Promise.reject(new Error("Invalid azp in token: "+c.azp));if(!u){var h=a+s,l=a-s;if(!c.iat)return i.Log.error("JoseUtil._validateJwt: iat was not provided"),Promise.reject(new Error("iat was not provided"));if(h>>((3&r)<<3)&255;return i}}},function(t,e){for(var r=[],n=0;n<256;++n)r[n]=(n+256).toString(16).substr(1);t.exports=function i(t,e){var n=e||0,i=r;return[i[t[n++]],i[t[n++]],i[t[n++]],i[t[n++]],"-",i[t[n++]],i[t[n++]],"-",i[t[n++]],i[t[n++]],"-",i[t[n++]],i[t[n++]],"-",i[t[n++]],i[t[n++]],i[t[n++]],i[t[n++]],i[t[n++]],i[t[n++]]].join("")}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SigninResponse=void 0;var n=function(){function t(t,e){for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:"#";!function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t);var o=i.UrlUtility.parseUrlFragment(e,r);this.error=o.error,this.error_description=o.error_description,this.error_uri=o.error_uri,this.code=o.code,this.state=o.state,this.id_token=o.id_token,this.session_state=o.session_state,this.access_token=o.access_token,this.token_type=o.token_type,this.scope=o.scope,this.profile=void 0,this.expires_in=o.expires_in}return n(t,[{key:"expires_in",get:function t(){if(this.expires_at){var e=parseInt(Date.now()/1e3);return this.expires_at-e}},set:function t(e){var r=parseInt(e);if("number"==typeof r&&r>0){var n=parseInt(Date.now()/1e3);this.expires_at=n+r}}},{key:"expired",get:function t(){var e=this.expires_in;if(void 0!==e)return e<=0}},{key:"scopes",get:function t(){return(this.scope||"").split(" ")}},{key:"isOpenIdConnect",get:function t(){return this.scopes.indexOf("openid")>=0||!!this.id_token}}]),t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SignoutRequest=void 0;var n=r(3),i=r(55),o=r(102);e.SignoutRequest=function t(e){var r=e.url,s=e.id_token_hint,a=e.post_logout_redirect_uri,u=e.data,c=e.extraQueryParams,f=e.request_type;if(function h(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!r)throw n.Log.error("SignoutRequest.ctor: No url passed"),new Error("url");for(var l in s&&(r=i.UrlUtility.addQueryParam(r,"id_token_hint",s)),a&&(r=i.UrlUtility.addQueryParam(r,"post_logout_redirect_uri",a),u&&(this.state=new o.State({data:u,request_type:f}),r=i.UrlUtility.addQueryParam(r,"state",this.state.id))),c)r=i.UrlUtility.addQueryParam(r,l,c[l]);this.url=r}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SignoutResponse=void 0;var n=r(55);e.SignoutResponse=function t(e){!function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t);var i=n.UrlUtility.parseUrlFragment(e,"?");this.error=i.error,this.error_description=i.error_description,this.error_uri=i.error_uri,this.state=i.state}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.InMemoryWebStorage=void 0;var n=function(){function t(t,e){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{},n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:c.SilentRenewService,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:f.SessionMonitor,a=arguments.length>3&&void 0!==arguments[3]?arguments[3]:h.TokenRevocationClient,d=arguments.length>4&&void 0!==arguments[4]?arguments[4]:l.TokenClient,g=arguments.length>5&&void 0!==arguments[5]?arguments[5]:p.JoseUtil;!function v(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,e),r instanceof s.UserManagerSettings||(r=new s.UserManagerSettings(r));var y=function m(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,r));return y._events=new u.UserManagerEvents(r),y._silentRenewService=new n(y),y.settings.automaticSilentRenew&&(i.Log.debug("UserManager.ctor: automaticSilentRenew is configured, setting up silent renew"),y.startSilentRenew()),y.settings.monitorSession&&(i.Log.debug("UserManager.ctor: monitorSession is configured, setting up session monitor"),y._sessionMonitor=new o(y)),y._tokenRevocationClient=new a(y._settings),y._tokenClient=new d(y._settings),y._joseUtil=g,y}return function r(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype.getUser=function t(){var e=this;return this._loadUser().then(function(t){return t?(i.Log.info("UserManager.getUser: user loaded"),e._events.load(t,!1),t):(i.Log.info("UserManager.getUser: user not found in storage"),null)})},e.prototype.removeUser=function t(){var e=this;return this.storeUser(null).then(function(){i.Log.info("UserManager.removeUser: user removed from storage"),e._events.unload()})},e.prototype.signinRedirect=function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};(e=Object.assign({},e)).request_type="si:r";var r={useReplaceToNavigate:e.useReplaceToNavigate};return this._signinStart(e,this._redirectNavigator,r).then(function(){i.Log.info("UserManager.signinRedirect: successful")})},e.prototype.signinRedirectCallback=function t(e){return this._signinEnd(e||this._redirectNavigator.url).then(function(t){return t.profile&&t.profile.sub?i.Log.info("UserManager.signinRedirectCallback: successful, signed in sub: ",t.profile.sub):i.Log.info("UserManager.signinRedirectCallback: no sub"),t})},e.prototype.signinPopup=function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};(e=Object.assign({},e)).request_type="si:p";var r=e.redirect_uri||this.settings.popup_redirect_uri||this.settings.redirect_uri;return r?(e.redirect_uri=r,e.display="popup",this._signin(e,this._popupNavigator,{startUrl:r,popupWindowFeatures:e.popupWindowFeatures||this.settings.popupWindowFeatures,popupWindowTarget:e.popupWindowTarget||this.settings.popupWindowTarget}).then(function(t){return t&&(t.profile&&t.profile.sub?i.Log.info("UserManager.signinPopup: signinPopup successful, signed in sub: ",t.profile.sub):i.Log.info("UserManager.signinPopup: no sub")),t})):(i.Log.error("UserManager.signinPopup: No popup_redirect_uri or redirect_uri configured"),Promise.reject(new Error("No popup_redirect_uri or redirect_uri configured")))},e.prototype.signinPopupCallback=function t(e){return this._signinCallback(e,this._popupNavigator).then(function(t){return t&&(t.profile&&t.profile.sub?i.Log.info("UserManager.signinPopupCallback: successful, signed in sub: ",t.profile.sub):i.Log.info("UserManager.signinPopupCallback: no sub")),t}).catch(function(t){i.Log.error(t.message)})},e.prototype.signinSilent=function t(){var e=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return(r=Object.assign({},r)).request_type="si:s",this._loadUser().then(function(t){return t&&t.refresh_token?(r.refresh_token=t.refresh_token,e._useRefreshToken(r)):(r.id_token_hint=r.id_token_hint||e.settings.includeIdTokenInSilentRenew&&t&&t.id_token,t&&e._settings.validateSubOnSilentRenew&&(i.Log.debug("UserManager.signinSilent, subject prior to silent renew: ",t.profile.sub),r.current_sub=t.profile.sub),e._signinSilentIframe(r))})},e.prototype._useRefreshToken=function t(){var e=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this._tokenClient.exchangeRefreshToken(r).then(function(t){return t?t.access_token?e._loadUser().then(function(r){if(r){var n=Promise.resolve();return t.id_token&&(n=e._validateIdTokenFromTokenRefreshToken(r.profile,t.id_token)),n.then(function(){return i.Log.debug("UserManager._useRefreshToken: refresh token response success"),r.id_token=t.id_token,r.access_token=t.access_token,r.refresh_token=t.refresh_token||r.refresh_token,r.expires_in=t.expires_in,e.storeUser(r).then(function(){return e._events.load(r),r})})}return null}):(i.Log.error("UserManager._useRefreshToken: No access token returned from token endpoint"),Promise.reject("No access token returned from token endpoint")):(i.Log.error("UserManager._useRefreshToken: No response returned from token endpoint"),Promise.reject("No response returned from token endpoint"))})},e.prototype._validateIdTokenFromTokenRefreshToken=function t(e,r){var n=this;return this._metadataService.getIssuer().then(function(t){return n._joseUtil.validateJwtAttributes(r,t,n._settings.client_id,n._settings.clockSkew).then(function(t){return t?t.sub!==e.sub?(i.Log.error("UserManager._validateIdTokenFromTokenRefreshToken: sub in id_token does not match current sub"),Promise.reject(new Error("sub in id_token does not match current sub"))):t.auth_time&&t.auth_time!==e.auth_time?(i.Log.error("UserManager._validateIdTokenFromTokenRefreshToken: auth_time in id_token does not match original auth_time"),Promise.reject(new Error("auth_time in id_token does not match original auth_time"))):t.azp&&t.azp!==e.azp?(i.Log.error("UserManager._validateIdTokenFromTokenRefreshToken: azp in id_token does not match original azp"),Promise.reject(new Error("azp in id_token does not match original azp"))):!t.azp&&e.azp?(i.Log.error("UserManager._validateIdTokenFromTokenRefreshToken: azp not in id_token, but present in original id_token"),Promise.reject(new Error("azp not in id_token, but present in original id_token"))):void 0:(i.Log.error("UserManager._validateIdTokenFromTokenRefreshToken: Failed to validate id_token"),Promise.reject(new Error("Failed to validate id_token")))})})},e.prototype._signinSilentIframe=function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=e.redirect_uri||this.settings.silent_redirect_uri||this.settings.redirect_uri;return r?(e.redirect_uri=r,e.prompt=e.prompt||"none",this._signin(e,this._iframeNavigator,{startUrl:r,silentRequestTimeout:e.silentRequestTimeout||this.settings.silentRequestTimeout}).then(function(t){return t&&(t.profile&&t.profile.sub?i.Log.info("UserManager.signinSilent: successful, signed in sub: ",t.profile.sub):i.Log.info("UserManager.signinSilent: no sub")),t})):(i.Log.error("UserManager.signinSilent: No silent_redirect_uri configured"),Promise.reject(new Error("No silent_redirect_uri configured")))},e.prototype.signinSilentCallback=function t(e){return this._signinCallback(e,this._iframeNavigator).then(function(t){return t&&(t.profile&&t.profile.sub?i.Log.info("UserManager.signinSilentCallback: successful, signed in sub: ",t.profile.sub):i.Log.info("UserManager.signinSilentCallback: no sub")),t})},e.prototype.signinCallback=function t(e){var r=this;return this.readSigninResponseState(e).then(function(t){var n=t.state;t.response;return"si:r"===n.request_type?r.signinRedirectCallback(e):"si:p"===n.request_type?r.signinPopupCallback(e):"si:s"===n.request_type?r.signinSilentCallback(e):Promise.reject(new Error("invalid response_type in state"))})},e.prototype.signoutCallback=function t(e,r){var n=this;return this.readSignoutResponseState(e).then(function(t){var i=t.state,o=t.response;return i?"so:r"===i.request_type?n.signoutRedirectCallback(e):"so:p"===i.request_type?n.signoutPopupCallback(e,r):Promise.reject(new Error("invalid response_type in state")):o})},e.prototype.querySessionStatus=function t(){var e=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};(r=Object.assign({},r)).request_type="si:s";var n=r.redirect_uri||this.settings.silent_redirect_uri||this.settings.redirect_uri;return n?(r.redirect_uri=n,r.prompt="none",r.response_type=r.response_type||this.settings.query_status_response_type,r.scope=r.scope||"openid",r.skipUserInfo=!0,this._signinStart(r,this._iframeNavigator,{startUrl:n,silentRequestTimeout:r.silentRequestTimeout||this.settings.silentRequestTimeout}).then(function(t){return e.processSigninResponse(t.url).then(function(t){if(i.Log.debug("UserManager.querySessionStatus: got signin response"),t.session_state&&t.profile.sub)return i.Log.info("UserManager.querySessionStatus: querySessionStatus success for sub: ",t.profile.sub),{session_state:t.session_state,sub:t.profile.sub,sid:t.profile.sid};i.Log.info("querySessionStatus successful, user not authenticated")})})):(i.Log.error("UserManager.querySessionStatus: No silent_redirect_uri configured"),Promise.reject(new Error("No silent_redirect_uri configured")))},e.prototype._signin=function t(e,r){var n=this,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this._signinStart(e,r,i).then(function(t){return n._signinEnd(t.url,e)})},e.prototype._signinStart=function t(e,r){var n=this,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return r.prepare(o).then(function(t){return i.Log.debug("UserManager._signinStart: got navigator window handle"),n.createSigninRequest(e).then(function(e){return i.Log.debug("UserManager._signinStart: got signin request"),o.url=e.url,o.id=e.state.id,t.navigate(o)}).catch(function(e){throw t.close&&(i.Log.debug("UserManager._signinStart: Error after preparing navigator, closing navigator window"),t.close()),e})})},e.prototype._signinEnd=function t(e){var r=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return this.processSigninResponse(e).then(function(t){i.Log.debug("UserManager._signinEnd: got signin response");var e=new a.User(t);if(n.current_sub){if(n.current_sub!==e.profile.sub)return i.Log.debug("UserManager._signinEnd: current user does not match user returned from signin. sub from signin: ",e.profile.sub),Promise.reject(new Error("login_required"));i.Log.debug("UserManager._signinEnd: current user matches user returned from signin")}return r.storeUser(e).then(function(){return i.Log.debug("UserManager._signinEnd: user stored"),r._events.load(e),e})})},e.prototype._signinCallback=function t(e,r){return i.Log.debug("UserManager._signinCallback"),r.callback(e)},e.prototype.signoutRedirect=function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};(e=Object.assign({},e)).request_type="so:r";var r=e.post_logout_redirect_uri||this.settings.post_logout_redirect_uri;r&&(e.post_logout_redirect_uri=r);var n={useReplaceToNavigate:e.useReplaceToNavigate};return this._signoutStart(e,this._redirectNavigator,n).then(function(){i.Log.info("UserManager.signoutRedirect: successful")})},e.prototype.signoutRedirectCallback=function t(e){return this._signoutEnd(e||this._redirectNavigator.url).then(function(t){return i.Log.info("UserManager.signoutRedirectCallback: successful"),t})},e.prototype.signoutPopup=function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};(e=Object.assign({},e)).request_type="so:p";var r=e.post_logout_redirect_uri||this.settings.popup_post_logout_redirect_uri||this.settings.post_logout_redirect_uri;return e.post_logout_redirect_uri=r,e.display="popup",e.post_logout_redirect_uri&&(e.state=e.state||{}),this._signout(e,this._popupNavigator,{startUrl:r,popupWindowFeatures:e.popupWindowFeatures||this.settings.popupWindowFeatures,popupWindowTarget:e.popupWindowTarget||this.settings.popupWindowTarget}).then(function(){i.Log.info("UserManager.signoutPopup: successful")})},e.prototype.signoutPopupCallback=function t(e,r){void 0===r&&"boolean"==typeof e&&(r=e,e=null);return this._popupNavigator.callback(e,r,"?").then(function(){i.Log.info("UserManager.signoutPopupCallback: successful")})},e.prototype._signout=function t(e,r){var n=this,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this._signoutStart(e,r,i).then(function(t){return n._signoutEnd(t.url)})},e.prototype._signoutStart=function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=this,n=arguments[1],o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.prepare(o).then(function(t){return i.Log.debug("UserManager._signoutStart: got navigator window handle"),r._loadUser().then(function(n){return i.Log.debug("UserManager._signoutStart: loaded current user from storage"),(r._settings.revokeAccessTokenOnSignout?r._revokeInternal(n):Promise.resolve()).then(function(){var s=e.id_token_hint||n&&n.id_token;return s&&(i.Log.debug("UserManager._signoutStart: Setting id_token into signout request"),e.id_token_hint=s),r.removeUser().then(function(){return i.Log.debug("UserManager._signoutStart: user removed, creating signout request"),r.createSignoutRequest(e).then(function(e){return i.Log.debug("UserManager._signoutStart: got signout request"),o.url=e.url,e.state&&(o.id=e.state.id),t.navigate(o)})})})}).catch(function(e){throw t.close&&(i.Log.debug("UserManager._signoutStart: Error after preparing navigator, closing navigator window"),t.close()),e})})},e.prototype._signoutEnd=function t(e){return this.processSignoutResponse(e).then(function(t){return i.Log.debug("UserManager._signoutEnd: got signout response"),t})},e.prototype.revokeAccessToken=function t(){var e=this;return this._loadUser().then(function(t){return e._revokeInternal(t,!0).then(function(r){if(r)return i.Log.debug("UserManager.revokeAccessToken: removing token properties from user and re-storing"),t.access_token=null,t.refresh_token=null,t.expires_at=null,t.token_type=null,e.storeUser(t).then(function(){i.Log.debug("UserManager.revokeAccessToken: user stored"),e._events.load(t)})})}).then(function(){i.Log.info("UserManager.revokeAccessToken: access token revoked successfully")})},e.prototype._revokeInternal=function t(e,r){var n=this;if(e){var o=e.access_token,s=e.refresh_token;return this._revokeAccessTokenInternal(o,r).then(function(t){return n._revokeRefreshTokenInternal(s,r).then(function(e){return t||e||i.Log.debug("UserManager.revokeAccessToken: no need to revoke due to no token(s), or JWT format"),t||e})})}return Promise.resolve(!1)},e.prototype._revokeAccessTokenInternal=function t(e,r){return!e||e.indexOf(".")>=0?Promise.resolve(!1):this._tokenRevocationClient.revoke(e,r).then(function(){return!0})},e.prototype._revokeRefreshTokenInternal=function t(e,r){return e?this._tokenRevocationClient.revoke(e,r,"refresh_token").then(function(){return!0}):Promise.resolve(!1)},e.prototype.startSilentRenew=function t(){this._silentRenewService.start()},e.prototype.stopSilentRenew=function t(){this._silentRenewService.stop()},e.prototype._loadUser=function t(){return this._userStore.get(this._userStoreKey).then(function(t){return t?(i.Log.debug("UserManager._loadUser: user storageString loaded"),a.User.fromStorageString(t)):(i.Log.debug("UserManager._loadUser: no user storageString"),null)})},e.prototype.storeUser=function t(e){if(e){i.Log.debug("UserManager.storeUser: storing user");var r=e.toStorageString();return this._userStore.set(this._userStoreKey,r)}return i.Log.debug("storeUser.storeUser: removing user"),this._userStore.remove(this._userStoreKey)},n(e,[{key:"_redirectNavigator",get:function t(){return this.settings.redirectNavigator}},{key:"_popupNavigator",get:function t(){return this.settings.popupNavigator}},{key:"_iframeNavigator",get:function t(){return this.settings.iframeNavigator}},{key:"_userStore",get:function t(){return this.settings.userStore}},{key:"events",get:function t(){return this._events}},{key:"_userStoreKey",get:function t(){return"user:"+this.settings.authority+":"+this.settings.client_id}}]),e}(o.OidcClient)},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.UserManagerSettings=void 0;var n=function(){function t(t,e){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{},n=r.popup_redirect_uri,i=r.popup_post_logout_redirect_uri,p=r.popupWindowFeatures,d=r.popupWindowTarget,g=r.silent_redirect_uri,v=r.silentRequestTimeout,y=r.automaticSilentRenew,m=void 0!==y&&y,_=r.validateSubOnSilentRenew,S=void 0!==_&&_,b=r.includeIdTokenInSilentRenew,w=void 0===b||b,F=r.monitorSession,E=void 0===F||F,x=r.checkSessionInterval,A=void 0===x?l:x,k=r.stopCheckSessionOnError,P=void 0===k||k,C=r.query_status_response_type,T=r.revokeAccessTokenOnSignout,R=void 0!==T&&T,I=r.accessTokenExpiringNotificationTime,O=void 0===I?h:I,D=r.redirectNavigator,N=void 0===D?new o.RedirectNavigator:D,L=r.popupNavigator,M=void 0===L?new s.PopupNavigator:L,j=r.iframeNavigator,U=void 0===j?new a.IFrameNavigator:j,B=r.userStore,H=void 0===B?new u.WebStorageStateStore({store:c.Global.sessionStorage}):B;!function V(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,e);var K=function q(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,arguments[0]));return K._popup_redirect_uri=n,K._popup_post_logout_redirect_uri=i,K._popupWindowFeatures=p,K._popupWindowTarget=d,K._silent_redirect_uri=g,K._silentRequestTimeout=v,K._automaticSilentRenew=m,K._validateSubOnSilentRenew=S,K._includeIdTokenInSilentRenew=w,K._accessTokenExpiringNotificationTime=O,K._monitorSession=E,K._checkSessionInterval=A,K._stopCheckSessionOnError=P,C?K._query_status_response_type=C:arguments[0]&&arguments[0].response_type?K._query_status_response_type=f.SigninRequest.isOidc(arguments[0].response_type)?"id_token":"code":K._query_status_response_type="id_token",K._revokeAccessTokenOnSignout=R,K._redirectNavigator=N,K._popupNavigator=M,K._iframeNavigator=U,K._userStore=H,K}return function r(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),n(e,[{key:"popup_redirect_uri",get:function t(){return this._popup_redirect_uri}},{key:"popup_post_logout_redirect_uri",get:function t(){return this._popup_post_logout_redirect_uri}},{key:"popupWindowFeatures",get:function t(){return this._popupWindowFeatures}},{key:"popupWindowTarget",get:function t(){return this._popupWindowTarget}},{key:"silent_redirect_uri",get:function t(){return this._silent_redirect_uri}},{key:"silentRequestTimeout",get:function t(){return this._silentRequestTimeout}},{key:"automaticSilentRenew",get:function t(){return this._automaticSilentRenew}},{key:"validateSubOnSilentRenew",get:function t(){return this._validateSubOnSilentRenew}},{key:"includeIdTokenInSilentRenew",get:function t(){return this._includeIdTokenInSilentRenew}},{key:"accessTokenExpiringNotificationTime",get:function t(){return this._accessTokenExpiringNotificationTime}},{key:"monitorSession",get:function t(){return this._monitorSession}},{key:"checkSessionInterval",get:function t(){return this._checkSessionInterval}},{key:"stopCheckSessionOnError",get:function t(){return this._stopCheckSessionOnError}},{key:"query_status_response_type",get:function t(){return this._query_status_response_type}},{key:"revokeAccessTokenOnSignout",get:function t(){return this._revokeAccessTokenOnSignout}},{key:"redirectNavigator",get:function t(){return this._redirectNavigator}},{key:"popupNavigator",get:function t(){return this._popupNavigator}},{key:"iframeNavigator",get:function t(){return this._iframeNavigator}},{key:"userStore",get:function t(){return this._userStore}}]),e}(i.OidcClientSettings)},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.RedirectNavigator=void 0;var n=function(){function t(t,e){for(var r=0;r1&&void 0!==arguments[1])||arguments[1];n.Log.debug("UserManagerEvents.load"),t.prototype.load.call(this,r),i&&this._userLoaded.raise(r)},e.prototype.unload=function e(){n.Log.debug("UserManagerEvents.unload"),t.prototype.unload.call(this),this._userUnloaded.raise()},e.prototype.addUserLoaded=function t(e){this._userLoaded.addHandler(e)},e.prototype.removeUserLoaded=function t(e){this._userLoaded.removeHandler(e)},e.prototype.addUserUnloaded=function t(e){this._userUnloaded.addHandler(e)},e.prototype.removeUserUnloaded=function t(e){this._userUnloaded.removeHandler(e)},e.prototype.addSilentRenewError=function t(e){this._silentRenewError.addHandler(e)},e.prototype.removeSilentRenewError=function t(e){this._silentRenewError.removeHandler(e)},e.prototype._raiseSilentRenewError=function t(e){n.Log.debug("UserManagerEvents._raiseSilentRenewError",e.message),this._silentRenewError.raise(e)},e.prototype.addUserSignedOut=function t(e){this._userSignedOut.addHandler(e)},e.prototype.removeUserSignedOut=function t(e){this._userSignedOut.removeHandler(e)},e.prototype._raiseUserSignedOut=function t(){n.Log.debug("UserManagerEvents._raiseUserSignedOut"),this._userSignedOut.raise()},e.prototype.addUserSessionChanged=function t(e){this._userSessionChanged.addHandler(e)},e.prototype.removeUserSessionChanged=function t(e){this._userSessionChanged.removeHandler(e)},e.prototype._raiseUserSessionChanged=function t(){n.Log.debug("UserManagerEvents._raiseUserSessionChanged"),this._userSessionChanged.raise()},e}(i.AccessTokenEvents)},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Timer=void 0;var n=function(){function t(t,e){for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:o.Global.timer,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0;!function s(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,e);var a=function u(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,r));return a._timer=n,a._nowFunc=i||function(){return Date.now()/1e3},a}return function r(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype.init=function t(e){e<=0&&(e=1),e=parseInt(e);var r=this.now+e;if(this.expiration===r&&this._timerHandle)i.Log.debug("Timer.init timer "+this._name+" skipping initialization since already initialized for expiration:",this.expiration);else{this.cancel(),i.Log.debug("Timer.init timer "+this._name+" for duration:",e),this._expiration=r;var n=5;e(() => roles_1.Add(role)); + } + + [Fact] + public void Should_do_nothing_if_role_to_add_is_default() + { + var roles_1 = roles_0.Add(Role.Developer); + + Assert.True(roles_1.CustomCount > 0); + } + + [Fact] + public void Should_update_role() + { + var roles_1 = roles_0.Update(firstRole, "P1", "P2"); + + roles_1[firstRole].Should().BeEquivalentTo(new Role(firstRole, new PermissionSet("P1", "P2"))); + } + + [Fact] + public void Should_return_same_roles_if_role_not_found() + { + var roles_1 = roles_0.Update(role, "P1", "P2"); + + Assert.Same(roles_0, roles_1); + } + + [Fact] + public void Should_remove_role() + { + var roles_1 = roles_0.Remove(firstRole); + + Assert.Equal(0, roles_1.CustomCount); + } + + [Fact] + public void Should_do_nothing_if_remove_role_not_found() + { + var roles_1 = roles_0.Remove(role); + + Assert.True(roles_1.CustomCount > 0); + } + + [Fact] + public void Should_get_custom_roles() + { + var names = roles_0.Custom.Select(x => x.Name).ToArray(); + + Assert.Equal(new[] { firstRole }, names); + } + + [Fact] + public void Should_get_all_roles() + { + var names = roles_0.All.Select(x => x.Name).ToArray(); + + Assert.Equal(new[] { firstRole, "Owner", "Reader", "Editor", "Developer" }, names); + } + + [Fact] + public void Should_check_for_custom_role() + { + Assert.True(roles_0.ContainsCustom(firstRole)); + } + + [Fact] + public void Should_check_for_non_custom_role() + { + Assert.False(roles_0.ContainsCustom(Role.Owner)); + } + + [Fact] + public void Should_check_for_default_role() + { + Assert.True(Roles.IsDefault(Role.Owner)); + } + + [Fact] + public void Should_check_for_non_default_role() + { + Assert.False(Roles.IsDefault(firstRole)); + } + + [InlineData("Developer")] + [InlineData("Editor")] + [InlineData("Owner")] + [InlineData("Reader")] + [Theory] + public void Should_get_default_roles(string name) + { + var found = roles_0.TryGet("app", name, out var role); + + Assert.True(found); + Assert.True(role!.IsDefault); + Assert.True(roles_0.Contains(name)); + + foreach (var permission in role.Permissions) + { + Assert.StartsWith("squidex.apps.app.", permission.Id); + } + } + + [Fact] + public void Should_return_null_if_role_not_found() + { + var found = roles_0.TryGet("app", "custom", out var role); + + Assert.False(found); + Assert.Null(role); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs new file mode 100644 index 000000000..3faa18603 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FluentAssertions; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Contents.Json; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Contents +{ + public class WorkflowJsonTests + { + [Fact] + public void Should_serialize_and_deserialize() + { + var workflow = Workflow.Default; + + var serialized = workflow.SerializeAndDeserialize(); + + serialized.Should().BeEquivalentTo(workflow); + } + + [Fact] + public void Should_verify_roles_mapping_in_workflow_transition() + { + var source = new JsonWorkflowTransition { Expression = "expression_1", Role = "role_1" }; + + var serialized = source.SerializeAndDeserialize(); + + var result = serialized.ToTransition(); + + Assert.Equal(new string[] { "role_1" }, result.Roles); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs new file mode 100644 index 000000000..c71529a9a --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs @@ -0,0 +1,147 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Collections; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Contents +{ + public class WorkflowTests + { + private readonly Workflow workflow = new Workflow( + Status.Draft, new Dictionary + { + [Status.Draft] = + new WorkflowStep( + new Dictionary + { + [Status.Archived] = new WorkflowTransition("ToArchivedExpr", ReadOnlyCollection.Create("ToArchivedRole" )), + [Status.Published] = new WorkflowTransition("ToPublishedExpr", ReadOnlyCollection.Create("ToPublishedRole" )) + }, + StatusColors.Draft), + [Status.Archived] = + new WorkflowStep(), + [Status.Published] = + new WorkflowStep() + }); + + [Fact] + public void Should_provide_default_workflow_if_none_found() + { + var result = Workflows.Empty.GetFirst(); + + Assert.Same(Workflow.Default, result); + } + + [Fact] + public void Should_provide_initial_state() + { + var (status, step) = workflow.GetInitialStep(); + + Assert.Equal(Status.Draft, status); + Assert.Equal(StatusColors.Draft, step.Color); + Assert.Same(workflow.Steps[Status.Draft], step); + } + + [Fact] + public void Should_provide_step() + { + var found = workflow.TryGetStep(Status.Draft, out var step); + + Assert.True(found); + Assert.Same(workflow.Steps[Status.Draft], step); + } + + [Fact] + public void Should_not_provide_unknown_step() + { + var found = workflow.TryGetStep(default, out var step); + + Assert.False(found); + Assert.Null(step); + } + + [Fact] + public void Should_provide_transition() + { + var found = workflow.TryGetTransition(Status.Draft, Status.Archived, out var transition); + + Assert.True(found); + Assert.Equal("ToArchivedExpr", transition!.Expression); + Assert.Equal(new[] { "ToArchivedRole" }, transition!.Roles); + } + + [Fact] + public void Should_provide_transition_to_initial_if_step_not_found() + { + var found = workflow.TryGetTransition(new Status("Other"), Status.Draft, out var transition); + + Assert.True(found); + Assert.Null(transition!.Expression); + Assert.Null(transition!.Roles); + } + + [Fact] + public void Should_not_provide_transition_from_unknown_step() + { + var found = workflow.TryGetTransition(new Status("Other"), Status.Archived, out var transition); + + Assert.False(found); + Assert.Null(transition); + } + + [Fact] + public void Should_not_provide_transition_to_unknown_step() + { + var found = workflow.TryGetTransition(Status.Draft, default, out var transition); + + Assert.False(found); + Assert.Null(transition); + } + + [Fact] + public void Should_provide_transitions() + { + var transitions = workflow.GetTransitions(Status.Draft).ToArray(); + + Assert.Equal(2, transitions.Length); + + var (status1, step1, transition1) = transitions[0]; + + Assert.Equal(Status.Archived, status1); + Assert.Equal("ToArchivedExpr", transition1.Expression); + + Assert.Equal(new[] { "ToArchivedRole" }, transition1.Roles); + Assert.Same(workflow.Steps[status1], step1); + + var (status2, step2, transition2) = transitions[1]; + + Assert.Equal(Status.Published, status2); + Assert.Equal("ToPublishedExpr", transition2.Expression); + Assert.Equal(new[] { "ToPublishedRole" }, transition2.Roles); + Assert.Same(workflow.Steps[status2], step2); + } + + [Fact] + public void Should_provide_transitions_to_initial_step_if_status_not_found() + { + var transitions = workflow.GetTransitions(new Status("Other")).ToArray(); + + Assert.Single(transitions); + + var (status1, step1, transition1) = transitions[0]; + + Assert.Equal(Status.Draft, status1); + Assert.Null(transition1.Expression); + Assert.Null(transition1.Roles); + Assert.Same(workflow.Steps[status1], step1); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs new file mode 100644 index 000000000..bd47797b5 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model +{ + public class PartitioningTests + { + [Fact] + public void Should_consider_null_as_valid_partitioning() + { + string? partitioning = null; + + Assert.True(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_consider_invariant_as_valid_partitioning() + { + var partitioning = "invariant"; + + Assert.True(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_consider_language_as_valid_partitioning() + { + var partitioning = "language"; + + Assert.True(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_not_consider_empty_as_valid_partitioning() + { + var partitioning = string.Empty; + + Assert.False(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_not_consider_other_string_as_valid_partitioning() + { + var partitioning = "invalid"; + + Assert.False(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_provide_invariant_instance() + { + Assert.Equal("invariant", Partitioning.Invariant.Key); + Assert.Equal("invariant", Partitioning.Invariant.ToString()); + } + + [Fact] + public void Should_provide_language_instance() + { + Assert.Equal("language", Partitioning.Language.Key); + Assert.Equal("language", Partitioning.Language.ToString()); + } + + [Fact] + public void Should_make_correct_equal_comparisons() + { + var partitioning1_a = new Partitioning("partitioning1"); + var partitioning1_b = new Partitioning("partitioning1"); + + var partitioning2 = new Partitioning("partitioning2"); + + Assert.Equal(partitioning1_a, partitioning1_b); + Assert.Equal(partitioning1_a.GetHashCode(), partitioning1_b.GetHashCode()); + Assert.True(partitioning1_a.Equals((object)partitioning1_b)); + + Assert.NotEqual(partitioning1_a, partitioning2); + Assert.NotEqual(partitioning1_a.GetHashCode(), partitioning2.GetHashCode()); + Assert.False(partitioning1_a.Equals((object)partitioning2)); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs new file mode 100644 index 000000000..352a3260d --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs @@ -0,0 +1,169 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Reflection; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Core.Model.Rules +{ + public class RuleTests + { + public static readonly List Triggers = + typeof(Rule).Assembly.GetTypes() + .Where(x => x.BaseType == typeof(RuleTrigger)) + .Select(Activator.CreateInstance) + .Select(x => new[] { x }) + .ToList()!; + + private readonly Rule rule_0 = new Rule(new ContentChangedTriggerV2(), new TestAction1()); + + public sealed class OtherTrigger : RuleTrigger + { + public override T Accept(IRuleTriggerVisitor visitor) + { + throw new NotSupportedException(); + } + } + + public sealed class MigratedTrigger : RuleTrigger, IMigrated + { + public override T Accept(IRuleTriggerVisitor visitor) + { + throw new NotSupportedException(); + } + + public RuleTrigger Migrate() + { + return new OtherTrigger(); + } + } + + [TypeName(nameof(TestAction1))] + public sealed class TestAction1 : RuleAction + { + public string Property { get; set; } + } + + [TypeName(nameof(TestAction2))] + public sealed class TestAction2 : RuleAction + { + public string Property { get; set; } + } + + [Fact] + public void Should_create_with_trigger_and_action() + { + var ruleTrigger = new ContentChangedTriggerV2(); + var ruleAction = new TestAction1(); + + var newRule = new Rule(ruleTrigger, ruleAction); + + Assert.Equal(ruleTrigger, newRule.Trigger); + Assert.Equal(ruleAction, newRule.Action); + Assert.True(newRule.IsEnabled); + } + + [Fact] + public void Should_set_enabled_to_true_when_enabling() + { + var rule_1 = rule_0.Disable(); + var rule_2 = rule_1.Enable(); + var rule_3 = rule_2.Enable(); + + Assert.False(rule_1.IsEnabled); + Assert.True(rule_3.IsEnabled); + } + + [Fact] + public void Should_set_enabled_to_false_when_disabling() + { + var rule_1 = rule_0.Disable(); + var rule_2 = rule_1.Disable(); + + Assert.True(rule_0.IsEnabled); + Assert.False(rule_2.IsEnabled); + } + + [Fact] + public void Should_replace_name_when_renaming() + { + var rule_1 = rule_0.Rename("MyName"); + + Assert.Equal("MyName", rule_1.Name); + } + + [Fact] + public void Should_replace_trigger_when_updating() + { + var newTrigger = new ContentChangedTriggerV2(); + + var rule_1 = rule_0.Update(newTrigger); + + Assert.NotSame(newTrigger, rule_0.Trigger); + Assert.Same(newTrigger, rule_1.Trigger); + } + + [Fact] + public void Should_throw_exception_when_new_trigger_has_other_type() + { + Assert.Throws(() => rule_0.Update(new OtherTrigger())); + } + + [Fact] + public void Should_replace_action_when_updating() + { + var newAction = new TestAction1(); + + var rule_1 = rule_0.Update(newAction); + + Assert.NotSame(newAction, rule_0.Action); + Assert.Same(newAction, rule_1.Action); + } + + [Fact] + public void Should_throw_exception_when_new_action_has_other_type() + { + Assert.Throws(() => rule_0.Update(new TestAction2())); + } + + [Fact] + public void Should_serialize_and_deserialize() + { + var rule_1 = rule_0.Disable(); + + var serialized = rule_1.SerializeAndDeserialize(); + + serialized.Should().BeEquivalentTo(rule_1); + } + + [Fact] + public void Should_serialize_and_deserialize_and_migrate_trigger() + { + var rule_X = new Rule(new MigratedTrigger(), new TestAction1()); + + var serialized = rule_X.SerializeAndDeserialize(); + + Assert.IsType(serialized.Trigger); + } + + [Theory] + [MemberData(nameof(Triggers))] + public void Should_freeze_triggers(RuleTrigger trigger) + { + TestUtils.TestFreeze(trigger); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs new file mode 100644 index 000000000..2ac4614a4 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Core.Model.Schemas +{ + public class SchemaFieldTests + { + public static readonly List FieldProperties = + typeof(Schema).Assembly.GetTypes() + .Where(x => x.BaseType == typeof(FieldProperties)) + .Select(Activator.CreateInstance) + .Select(x => new[] { x }) + .ToList()!; + + private readonly RootField field_0 = Fields.Number(1, "my-field", Partitioning.Invariant); + + [Fact] + public void Should_instantiate_field() + { + Assert.True(field_0.RawProperties.IsFrozen); + Assert.Equal("my-field", field_0.Name); + } + + [Fact] + public void Should_throw_exception_if_creating_field_with_invalid_name() + { + Assert.Throws(() => Fields.Number(1, string.Empty, Partitioning.Invariant)); + } + + [Fact] + public void Should_hide_field() + { + var field_1 = field_0.Hide(); + var field_2 = field_1.Hide(); + + Assert.False(field_0.IsHidden); + Assert.True(field_2.IsHidden); + } + + [Fact] + public void Should_show_field() + { + var field_1 = field_0.Hide(); + var field_2 = field_1.Show(); + var field_3 = field_2.Show(); + + Assert.True(field_1.IsHidden); + Assert.False(field_3.IsHidden); + } + + [Fact] + public void Should_disable_field() + { + var field_1 = field_0.Disable(); + var field_2 = field_1.Disable(); + + Assert.False(field_0.IsDisabled); + Assert.True(field_2.IsDisabled); + } + + [Fact] + public void Should_enable_field() + { + var field_1 = field_0.Disable(); + var field_2 = field_1.Enable(); + var field_3 = field_2.Enable(); + + Assert.True(field_1.IsDisabled); + Assert.False(field_3.IsDisabled); + } + + [Fact] + public void Should_lock_field() + { + var field_1 = field_0.Lock(); + + Assert.False(field_0.IsLocked); + Assert.True(field_1.IsLocked); + } + + [Fact] + public void Should_update_field() + { + var field_1 = field_0.Update(new NumberFieldProperties { Hints = "my-hints" }); + + Assert.Null(field_0.RawProperties.Hints); + Assert.True(field_1.RawProperties.IsFrozen); + Assert.Equal("my-hints", field_1.RawProperties.Hints); + } + + [Fact] + public void Should_throw_exception_if_updating_with_invalid_properties_type() + { + Assert.Throws(() => field_0.Update(new StringFieldProperties())); + } + + [Theory] + [MemberData(nameof(FieldProperties))] + public void Should_freeze_field_properties(FieldProperties action) + { + TestUtils.TestFreeze(action); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs new file mode 100644 index 000000000..bd7eea917 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs @@ -0,0 +1,148 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +#pragma warning disable xUnit2013 // Do not use equality check to check for collection size. + +namespace Squidex.Domain.Apps.Core.Operations.ConvertContent +{ + public class ContentConversionFlatTests + { + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); + + [Fact] + public void Should_return_original_when_no_language_preferences_defined() + { + var data = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 1)); + + Assert.Same(data, data.ToFlatLanguageModel(languagesConfig)); + } + + [Fact] + public void Should_return_flatten_value() + { + var data = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("de", 1) + .AddValue("en", 2)) + .AddField("field2", + new ContentFieldData() + .AddValue("de", JsonValue.Null) + .AddValue("en", 4)) + .AddField("field3", + new ContentFieldData() + .AddValue("en", 6)) + .AddField("field4", + new ContentFieldData() + .AddValue("it", 7)); + + var output = data.ToFlatten(); + + var expected = new Dictionary + { + { + "field1", + new ContentFieldData() + .AddValue("de", 1) + .AddValue("en", 2) + }, + { + "field2", + new ContentFieldData() + .AddValue("de", JsonValue.Null) + .AddValue("en", 4) + }, + { "field3", JsonValue.Create(6) }, + { "field4", JsonValue.Create(7) } + }; + + Assert.True(expected.EqualsDictionary(output)); + } + + [Fact] + public void Should_return_flat_list_when_single_languages_specified() + { + var data = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("de", 1) + .AddValue("en", 2)) + .AddField("field2", + new ContentFieldData() + .AddValue("de", JsonValue.Null) + .AddValue("en", 4)) + .AddField("field3", + new ContentFieldData() + .AddValue("en", 6)) + .AddField("field4", + new ContentFieldData() + .AddValue("it", 7)); + + var fallbackConfig = + LanguagesConfig.Build( + new LanguageConfig(Language.EN), + new LanguageConfig(Language.DE, false, Language.EN)); + + var output = (Dictionary)data.ToFlatLanguageModel(fallbackConfig, new List { Language.DE }); + + var expected = new Dictionary + { + { "field1", JsonValue.Create(1) }, + { "field2", JsonValue.Create(4) }, + { "field3", JsonValue.Create(6) } + }; + + Assert.True(expected.EqualsDictionary(output)); + } + + [Fact] + public void Should_return_flat_list_when_languages_specified() + { + var data = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("de", 1) + .AddValue("en", 2)) + .AddField("field2", + new ContentFieldData() + .AddValue("de", JsonValue.Null) + .AddValue("en", 4)) + .AddField("field3", + new ContentFieldData() + .AddValue("en", 6)) + .AddField("field4", + new ContentFieldData() + .AddValue("it", 7)); + + var output = (Dictionary)data.ToFlatLanguageModel(languagesConfig, new List { Language.DE, Language.EN }); + + var expected = new Dictionary + { + { "field1", JsonValue.Create(1) }, + { "field2", JsonValue.Create(4) }, + { "field3", JsonValue.Create(6) } + }; + + Assert.True(expected.EqualsDictionary(output)); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs new file mode 100644 index 000000000..6ee85e7a5 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs @@ -0,0 +1,198 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.EnrichContent; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +#pragma warning disable xUnit2004 // Do not use equality check to test for boolean conditions + +namespace Squidex.Domain.Apps.Core.Operations.EnrichContent +{ + public class ContentEnrichmentTests + { + private readonly Instant now = Instant.FromUtc(2017, 10, 12, 16, 30, 10); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); + private readonly Schema schema; + + public ContentEnrichmentTests() + { + schema = + new Schema("my-schema") + .AddString(1, "my-string", Partitioning.Language, + new StringFieldProperties { DefaultValue = "en-string" }) + .AddNumber(2, "my-number", Partitioning.Invariant, + new NumberFieldProperties()) + .AddDateTime(3, "my-datetime", Partitioning.Invariant, + new DateTimeFieldProperties { DefaultValue = now }) + .AddBoolean(4, "my-boolean", Partitioning.Invariant, + new BooleanFieldProperties { DefaultValue = true }); + } + + [Fact] + public void Should_enrich_with_default_values() + { + var data = + new NamedContentData() + .AddField("my-string", + new ContentFieldData() + .AddValue("de", "de-string")) + .AddField("my-number", + new ContentFieldData() + .AddValue("iv", 456)); + + data.Enrich(schema, languagesConfig.ToResolver()); + + Assert.Equal(456, ((JsonScalar)data["my-number"]!["iv"]).Value); + + Assert.Equal("de-string", data["my-string"]!["de"].ToString()); + Assert.Equal("en-string", data["my-string"]!["en"].ToString()); + + Assert.Equal(now.ToString(), data["my-datetime"]!["iv"].ToString()); + + Assert.True(((JsonScalar)data["my-boolean"]!["iv"]).Value); + } + + [Fact] + public void Should_also_enrich_with_default_values_when_string_is_empty() + { + var data = + new NamedContentData() + .AddField("my-string", + new ContentFieldData() + .AddValue("de", string.Empty)) + .AddField("my-number", + new ContentFieldData() + .AddValue("iv", 456)); + + data.Enrich(schema, languagesConfig.ToResolver()); + + Assert.Equal("en-string", data["my-string"]!["de"].ToString()); + Assert.Equal("en-string", data["my-string"]!["en"].ToString()); + } + + [Fact] + public void Should_get_default_value_from_assets_field() + { + var field = + Fields.Assets(1, "1", Partitioning.Invariant, + new AssetsFieldProperties()); + + Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_boolean_field() + { + var field = + Fields.Boolean(1, "1", Partitioning.Invariant, + new BooleanFieldProperties { DefaultValue = true }); + + Assert.Equal(JsonValue.True, DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field() + { + var field = + Fields.DateTime(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { DefaultValue = FutureDays(15) }); + + Assert.Equal(JsonValue.Create(FutureDays(15).ToString()), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field_when_set_to_today() + { + var field = + Fields.DateTime(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Today }); + + Assert.Equal(JsonValue.Create("2017-10-12T00:00:00Z"), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field_when_set_to_now() + { + var field = + Fields.DateTime(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now }); + + Assert.Equal(JsonValue.Create("2017-10-12T16:30:10Z"), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_json_field() + { + var field = + Fields.Json(1, "1", Partitioning.Invariant, + new JsonFieldProperties()); + + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_geolocation_field() + { + var field = + Fields.Geolocation(1, "1", Partitioning.Invariant, + new GeolocationFieldProperties()); + + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_number_field() + { + var field = + Fields.Number(1, "1", Partitioning.Invariant, + new NumberFieldProperties { DefaultValue = 12 }); + + Assert.Equal(JsonValue.Create(12), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_references_field() + { + var field = + Fields.References(1, "1", Partitioning.Invariant, + new ReferencesFieldProperties()); + + Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_string_field() + { + var field = + Fields.String(1, "1", Partitioning.Invariant, + new StringFieldProperties { DefaultValue = "default" }); + + Assert.Equal(JsonValue.Create("default"), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_tags_field() + { + var field = + Fields.Tags(1, "1", Partitioning.Invariant, + new TagsFieldProperties()); + + Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + private Instant FutureDays(int days) + { + return now.WithoutMs().Plus(Duration.FromDays(days)); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs new file mode 100644 index 000000000..491fdf39d --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs @@ -0,0 +1,606 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.EventSynchronization; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization +{ + public class SchemaSynchronizerTests + { + private readonly Func idGenerator; + private readonly IJsonSerializer jsonSerializer = TestUtils.DefaultSerializer; + private readonly NamedId stringId = NamedId.Of(13L, "my-value"); + private readonly NamedId nestedId = NamedId.Of(141L, "my-value"); + private readonly NamedId arrayId = NamedId.Of(14L, "11-array"); + private int fields = 50; + + public SchemaSynchronizerTests() + { + idGenerator = () => fields++; + } + + [Fact] + public void Should_create_events_if_schema_deleted() + { + var sourceSchema = + new Schema("source"); + + var targetSchema = + (Schema?)null; + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaDeleted() + ); + } + + [Fact] + public void Should_create_events_if_category_changed() + { + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target") + .ChangeCategory("Category"); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaCategoryChanged { Name = "Category" } + ); + } + + [Fact] + public void Should_create_events_if_scripts_configured() + { + var scripts = new SchemaScripts + { + Create = "" + }; + + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target").ConfigureScripts(scripts); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaScriptsConfigured { Scripts = scripts } + ); + } + + [Fact] + public void Should_create_events_if_preview_urls_configured() + { + var previewUrls = new Dictionary + { + ["web"] = "Url" + }; + + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target") + .ConfigurePreviewUrls(previewUrls); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaPreviewUrlsConfigured { PreviewUrls = previewUrls } + ); + } + + [Fact] + public void Should_create_events_if_schema_published() + { + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target") + .Publish(); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaPublished() + ); + } + + [Fact] + public void Should_create_events_if_schema_unpublished() + { + var sourceSchema = + new Schema("source") + .Publish(); + + var targetSchema = + new Schema("target"); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaUnpublished() + ); + } + + [Fact] + public void Should_create_events_if_nested_field_deleted() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_deleted() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target"); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_updated() + { + var properties = new StringFieldProperties { IsRequired = true }; + + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name, properties)); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldUpdated { Properties = properties, FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_updated() + { + var properties = new StringFieldProperties { IsRequired = true }; + + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant, properties); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldUpdated { Properties = properties, FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_locked() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .LockField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldLocked { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_locked() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .LockField(stringId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldLocked { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_hidden() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .HideField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldHidden { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_hidden() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .HideField(stringId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldHidden { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_shown() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .HideField(nestedId.Id, arrayId.Id); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldShown { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_shown() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .HideField(stringId.Id); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldShown { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_disabled() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .DisableField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDisabled { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_disabled() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .DisableField(stringId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDisabled { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_enabled() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .DisableField(nestedId.Id, arrayId.Id); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldEnabled { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_enabled() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .DisableField(stringId.Id); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldEnabled { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_field_created() + { + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .HideField(stringId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + var createdId = NamedId.Of(50L, stringId.Name); + + events.ShouldHaveSameEvents( + new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, + new FieldHidden { FieldId = createdId } + ); + } + + [Fact] + public void Should_create_events_if_field_type_has_changed() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddTags(stringId.Id, stringId.Name, Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + var createdId = NamedId.Of(50L, stringId.Name); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = stringId }, + new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new TagsFieldProperties() } + ); + } + + [Fact] + public void Should_create_events_if_field_partitioning_has_changed() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Language); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + var createdId = NamedId.Of(50L, stringId.Name); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = stringId }, + new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Language.Key, Properties = new StringFieldProperties() } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_created() + { + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .HideField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + var id1 = NamedId.Of(50L, arrayId.Name); + var id2 = NamedId.Of(51L, stringId.Name); + + events.ShouldHaveSameEvents( + new FieldAdded { FieldId = id1, Name = arrayId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new ArrayFieldProperties() }, + new FieldAdded { FieldId = id2, Name = stringId.Name, ParentFieldId = id1, Properties = new StringFieldProperties() }, + new FieldHidden { FieldId = id2, ParentFieldId = id1 } + ); + } + + [Fact] + public void Should_create_events_if_nested_fields_reordered() + { + var id1 = NamedId.Of(1, "f1"); + var id2 = NamedId.Of(2, "f1"); + + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(10, "f1") + .AddString(11, "f2")); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(1, "f2") + .AddString(2, "f1")); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaFieldsReordered { FieldIds = new List { 11, 10 }, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_fields_reordered() + { + var id1 = NamedId.Of(1, "f1"); + var id2 = NamedId.Of(2, "f1"); + + var sourceSchema = + new Schema("source") + .AddString(10, "f1", Partitioning.Invariant) + .AddString(11, "f2", Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(1, "f2", Partitioning.Invariant) + .AddString(2, "f1", Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaFieldsReordered { FieldIds = new List { 11, 10 } } + ); + } + + [Fact] + public void Should_create_events_if_fields_reordered_after_sync() + { + var id1 = NamedId.Of(1, "f1"); + var id2 = NamedId.Of(2, "f1"); + + var sourceSchema = + new Schema("source") + .AddString(10, "f1", Partitioning.Invariant) + .AddString(11, "f2", Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(1, "f3", Partitioning.Invariant) + .AddString(2, "f1", Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = NamedId.Of(11L, "f2") }, + new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, + new SchemaFieldsReordered { FieldIds = new List { 50, 10 } } + ); + } + + [Fact] + public void Should_create_events_if_fields_reordered_after_sync2() + { + var id1 = NamedId.Of(1, "f1"); + var id2 = NamedId.Of(2, "f1"); + + var sourceSchema = + new Schema("source") + .AddString(10, "f1", Partitioning.Invariant) + .AddString(11, "f2", Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(1, "f1", Partitioning.Invariant) + .AddString(2, "f3", Partitioning.Invariant) + .AddString(3, "f2", Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, + new SchemaFieldsReordered { FieldIds = new List { 10, 50, 11 } } + ); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs new file mode 100644 index 000000000..f4282c76c --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs @@ -0,0 +1,308 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.ExtractReferenceIds; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +#pragma warning disable xUnit2013 // Do not use equality check to check for collection size. + +namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds +{ + public class ReferenceExtractionTests + { + private readonly Guid schemaId = Guid.NewGuid(); + private readonly Schema schema; + + public ReferenceExtractionTests() + { + schema = + new Schema("my-schema") + .AddNumber(1, "field1", Partitioning.Language) + .AddNumber(2, "field2", Partitioning.Invariant) + .AddNumber(3, "field3", Partitioning.Invariant) + .AddAssets(5, "assets1", Partitioning.Invariant) + .AddAssets(6, "assets2", Partitioning.Invariant) + .AddArray(7, "array", Partitioning.Invariant, a => a + .AddAssets(71, "assets71")) + .AddJson(4, "json", Partitioning.Language) + .UpdateField(3, f => f.Hide()); + } + + [Fact] + public void Should_get_ids_from_id_data() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var input = + new IdContentData() + .AddField(5, + new ContentFieldData() + .AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString()))); + + var ids = input.GetReferencedIds(schema).ToArray(); + + Assert.Equal(new[] { id1, id2 }, ids); + } + + [Fact] + public void Should_get_ids_from_name_data() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var input = + new NamedContentData() + .AddField("assets1", + new ContentFieldData() + .AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString()))); + + var ids = input.GetReferencedIds(schema).ToArray(); + + Assert.Equal(new[] { id1, id2 }, ids); + } + + [Fact] + public void Should_cleanup_deleted_ids() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var input = + new IdContentData() + .AddField(5, + new ContentFieldData() + .AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString()))); + + var converter = FieldConverters.ForValues(ValueReferencesConverter.CleanReferences(new[] { id2 })); + + var actual = input.ConvertId2Id(schema, converter); + + var cleanedValue = (JsonArray)actual[5]!["iv"]; + + Assert.Equal(1, cleanedValue.Count); + Assert.Equal(id1.ToString(), cleanedValue[0].ToString()); + } + + [Fact] + public void Should_return_ids_from_assets_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); + + var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray(); + + Assert.Equal(new[] { id1, id2 }, result); + } + + [Fact] + public void Should_return_empty_list_from_assets_field_for_referenced_ids_when_null() + { + var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); + + var result = sut.GetReferencedIds(null).ToArray(); + + Assert.Empty(result); + } + + [Fact] + public void Should_return_empty_list_from_assets_field_for_referenced_ids_when_other_type() + { + var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); + + var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray(); + + Assert.Empty(result); + } + + [Fact] + public void Should_return_empty_list_from_non_references_field() + { + var sut = Fields.String(1, "my-string", Partitioning.Invariant); + + var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray(); + + Assert.Empty(result); + } + + [Fact] + public void Should_return_null_from_assets_field_when_removing_references_from_null_array() + { + var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); + + var result = sut.CleanReferences(JsonValue.Null, null); + + Assert.Equal(JsonValue.Null, result); + } + + [Fact] + public void Should_remove_deleted_references_from_assets_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); + + var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(id2)); + + Assert.Equal(CreateValue(id1), result); + } + + [Fact] + public void Should_return_same_token_from_assets_field_when_removing_references_and_nothing_to_remove() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); + + var token = CreateValue(id1, id2); + + var result = sut.CleanReferences(token, HashSet.Of(Guid.NewGuid())); + + Assert.Same(token, result); + } + + [Fact] + public void Should_return_ids_from_nested_references_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = + Fields.Array(1, "my-array", Partitioning.Invariant, + Fields.References(1, "my-refs", + new ReferencesFieldProperties { SchemaId = schemaId })); + + var value = + JsonValue.Array( + JsonValue.Object() + .Add("my-refs", CreateValue(id1, id2))); + + var result = sut.GetReferencedIds(value).ToArray(); + + Assert.Equal(new[] { id1, id2, schemaId }, result); + } + + [Fact] + public void Should_return_ids_from_references_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.References(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray(); + + Assert.Equal(new[] { id1, id2, schemaId }, result); + } + + [Fact] + public void Should_return_ids_from_references_field_without_schema_id() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.References(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.GetReferencedIds(CreateValue(id1, id2), Ids.ContentOnly).ToArray(); + + Assert.Equal(new[] { id1, id2 }, result); + } + + [Fact] + public void Should_return_list_from_references_field_with_schema_id_list_for_referenced_ids_when_null() + { + var sut = Fields.References(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.GetReferencedIds(JsonValue.Null).ToArray(); + + Assert.Equal(new[] { schemaId }, result); + } + + [Fact] + public void Should_return_list_from_references_field_with_schema_id_for_referenced_ids_when_other_type() + { + var sut = Fields.References(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray(); + + Assert.Equal(new[] { schemaId }, result); + } + + [Fact] + public void Should_return_null_from_references_field_when_removing_references_from_null_array() + { + var sut = Fields.References(1, "my-refs", Partitioning.Invariant); + + var result = sut.CleanReferences(JsonValue.Null, null); + + Assert.Equal(JsonValue.Null, result); + } + + [Fact] + public void Should_remove_deleted_references_from_references_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.References(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(id2)); + + Assert.Equal(CreateValue(id1), result); + } + + [Fact] + public void Should_remove_all_references_from_references_field_when_schema_is_removed() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.References(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(schemaId)); + + Assert.Equal(CreateValue(), result); + } + + [Fact] + public void Should_return_same_token_from_references_field_when_removing_references_and_nothing_to_remove() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.References(1, "my-refs", Partitioning.Invariant); + + var value = CreateValue(id1, id2); + + var result = sut.CleanReferences(value, HashSet.Of(Guid.NewGuid())); + + Assert.Same(value, result); + } + + private static IJsonValue CreateValue(params Guid[] ids) + { + return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceFormattingTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceFormattingTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceFormattingTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceFormattingTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleElementRegistryTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleElementRegistryTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleElementRegistryTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleElementRegistryTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs new file mode 100644 index 000000000..630429be8 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -0,0 +1,331 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.Extensions.Options; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; +using Xunit; + +#pragma warning disable xUnit2009 // Do not use boolean check to check for string equality + +namespace Squidex.Domain.Apps.Core.Operations.HandleRules +{ + public class RuleServiceTests + { + private readonly IRuleTriggerHandler ruleTriggerHandler = A.Fake(); + private readonly IRuleActionHandler ruleActionHandler = A.Fake(); + private readonly IEventEnricher eventEnricher = A.Fake(); + private readonly IClock clock = A.Fake(); + private readonly string actionData = "{\"value\":10}"; + private readonly string actionDump = "MyDump"; + private readonly string actionName = "ValidAction"; + private readonly string actionDescription = "MyDescription"; + private readonly Guid ruleId = Guid.NewGuid(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); + private readonly RuleService sut; + + public sealed class InvalidEvent : IEvent + { + } + + public sealed class InvalidAction : RuleAction + { + } + + public sealed class ValidAction : RuleAction + { + } + + public sealed class ValidData + { + public int Value { get; set; } + } + + public sealed class InvalidTrigger : RuleTrigger + { + public override T Accept(IRuleTriggerVisitor visitor) + { + return default!; + } + } + + public RuleServiceTests() + { + typeNameRegistry.Map(typeof(ContentCreated)); + typeNameRegistry.Map(typeof(ValidAction), actionName); + + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); + + A.CallTo(() => ruleActionHandler.ActionType) + .Returns(typeof(ValidAction)); + + A.CallTo(() => ruleActionHandler.DataType) + .Returns(typeof(ValidData)); + + A.CallTo(() => ruleTriggerHandler.TriggerType) + .Returns(typeof(ContentChangedTriggerV2)); + + var log = A.Fake(); + + sut = new RuleService(Options.Create(new RuleOptions()), + new[] { ruleTriggerHandler }, + new[] { ruleActionHandler }, + eventEnricher, TestUtils.DefaultSerializer, clock, log, typeNameRegistry); + } + + [Fact] + public async Task Should_not_create_job_if_rule_disabled() + { + var @event = Envelope.Create(new ContentCreated()); + + var job = await sut.CreateJobAsync(ValidRule().Disable(), ruleId, @event); + + Assert.Null(job); + + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_create_job_for_invalid_event() + { + var @event = Envelope.Create(new InvalidEvent()); + + var job = await sut.CreateJobAsync(ValidRule(), ruleId, @event); + + Assert.Null(job); + + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_create_job_if_no_trigger_handler_registered() + { + var @event = Envelope.Create(new ContentCreated()); + + var job = await sut.CreateJobAsync(RuleInvalidTrigger(), ruleId, @event); + + Assert.Null(job); + + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_create_job_if_no_action_handler_registered() + { + var @event = Envelope.Create(new ContentCreated()); + + var job = await sut.CreateJobAsync(RuleInvalidAction(), ruleId, @event); + + Assert.Null(job); + + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_create_job_if_too_old() + { + var @event = Envelope.Create(new ContentCreated()).SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); + + var job = await sut.CreateJobAsync(ValidRule(), ruleId, @event); + + Assert.Null(job); + + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_create_job_if_not_triggered_with_precheck() + { + var rule = ValidRule(); + + var @event = Envelope.Create(new ContentCreated()); + + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) + .Returns(false); + + var job = await sut.CreateJobAsync(rule, ruleId, @event); + + Assert.Null(job); + + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_create_job_if_enriched_event_not_created() + { + var rule = ValidRule(); + + var @event = Envelope.Create(new ContentCreated()); + + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) + .Returns(true); + + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) + .Returns(Task.FromResult(null)); + + var job = await sut.CreateJobAsync(rule, ruleId, @event); + + Assert.Null(job); + } + + [Fact] + public async Task Should_not_create_job_if_not_triggered() + { + var rule = ValidRule(); + + var enrichedEvent = new EnrichedContentEvent { AppId = appId }; + + var @event = Envelope.Create(new ContentCreated()); + + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) + .Returns(true); + + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) + .Returns(enrichedEvent); + + A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger)) + .Returns(false); + + var job = await sut.CreateJobAsync(rule, ruleId, @event); + + Assert.Null(job); + } + + [Fact] + public async Task Should_create_job_if_triggered() + { + var now = clock.GetCurrentInstant(); + + var rule = ValidRule(); + + var enrichedEvent = new EnrichedContentEvent { AppId = appId }; + + var @event = Envelope.Create(new ContentCreated()).SetTimestamp(now); + + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) + .Returns(true); + + A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger)) + .Returns(true); + + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) + .Returns(enrichedEvent); + + A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, rule.Action)) + .Returns((actionDescription, new ValidData { Value = 10 })); + + var job = (await sut.CreateJobAsync(rule, ruleId, @event))!; + + Assert.Equal(actionData, job.ActionData); + Assert.Equal(actionName, job.ActionName); + Assert.Equal(actionDescription, job.Description); + + Assert.Equal(now, job.Created); + Assert.Equal(now.Plus(Duration.FromDays(30)), job.Expires); + + Assert.Equal(enrichedEvent.AppId.Id, job.AppId); + + Assert.NotEqual(Guid.Empty, job.Id); + + A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent, A>.That.Matches(x => x.Payload == @event.Payload))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_succeeded_job_with_full_dump_when_handler_returns_no_exception() + { + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) + .Returns(Result.Success(actionDump)); + + var result = await sut.InvokeAsync(actionName, actionData); + + Assert.Equal(RuleResult.Success, result.Result.Status); + + Assert.True(result.Elapsed >= TimeSpan.Zero); + Assert.True(result.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task Should_return_failed_job_with_full_dump_when_handler_returns_exception() + { + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) + .Returns(Result.Failed(new InvalidOperationException(), actionDump)); + + var result = await sut.InvokeAsync(actionName, actionData); + + Assert.Equal(RuleResult.Failed, result.Result.Status); + + Assert.True(result.Elapsed >= TimeSpan.Zero); + Assert.True(result.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task Should_return_timedout_job_with_full_dump_when_exception_from_handler_indicates_timeout() + { + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) + .Returns(Result.Failed(new TimeoutException(), actionDump)); + + var result = await sut.InvokeAsync(actionName, actionData); + + Assert.Equal(RuleResult.Timeout, result.Result.Status); + + Assert.True(result.Elapsed >= TimeSpan.Zero); + Assert.True(result.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); + + Assert.True(result.Result.Dump?.IndexOf("Action timed out.", StringComparison.OrdinalIgnoreCase) >= 0); + } + + [Fact] + public async Task Should_create_exception_details_when_job_to_execute_failed() + { + var ex = new InvalidOperationException(); + + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) + .Throws(ex); + + var result = await sut.InvokeAsync(actionName, actionData); + + Assert.Equal(ex, result.Result.Exception); + } + + private static Rule RuleInvalidAction() + { + return new Rule(new ContentChangedTriggerV2(), new InvalidAction()); + } + + private static Rule RuleInvalidTrigger() + { + return new Rule(new InvalidTrigger(), new ValidAction()); + } + + private static Rule ValidRule() + { + return new Rule(new ContentChangedTriggerV2(), new ValidAction()); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs new file mode 100644 index 000000000..86d1690c8 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs @@ -0,0 +1,134 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +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.Tags; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.Tags +{ + public class TagNormalizerTests + { + private readonly ITagService tagService = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + private readonly Guid schemaId = Guid.NewGuid(); + private readonly Schema schema; + + public TagNormalizerTests() + { + schema = + new Schema("my-schema") + .AddTags(1, "tags1", Partitioning.Invariant) + .AddTags(2, "tags2", Partitioning.Invariant, new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) + .AddString(3, "string", Partitioning.Invariant) + .AddArray(4, "array", Partitioning.Invariant, f => f + .AddTags(401, "nestedTags1") + .AddTags(402, "nestedTags2", new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) + .AddString(403, "string")); + } + + [Fact] + public async Task Should_normalize_tags_with_old_data() + { + var newData = GenerateData("n_raw"); + var oldData = GenerateData("o_raw"); + + A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), + A>.That.IsSameSequenceAs("n_raw2_1", "n_raw2_2", "n_raw4"), + A>.That.IsSameSequenceAs("o_raw2_1", "o_raw2_2", "o_raw4"))) + .Returns(new Dictionary + { + ["n_raw2_2"] = "id2_2", + ["n_raw2_1"] = "id2_1", + ["n_raw4"] = "id4" + }); + + await tagService.NormalizeAsync(appId, schemaId, schema, newData, oldData); + + Assert.Equal(JsonValue.Array("id2_1", "id2_2"), newData["tags2"]!["iv"]); + Assert.Equal(JsonValue.Array("id4"), GetNestedTags(newData)); + } + + [Fact] + public async Task Should_normalize_tags_without_old_data() + { + var newData = GenerateData("name"); + + A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), + A>.That.IsSameSequenceAs("name2_1", "name2_2", "name4"), + A>.That.IsEmpty())) + .Returns(new Dictionary + { + ["name2_2"] = "id2_2", + ["name2_1"] = "id2_1", + ["name4"] = "id4" + }); + + await tagService.NormalizeAsync(appId, schemaId, schema, newData, null); + + Assert.Equal(JsonValue.Array("id2_1", "id2_2"), newData["tags2"]!["iv"]); + Assert.Equal(JsonValue.Array("id4"), GetNestedTags(newData)); + } + + [Fact] + public async Task Should_denormalize_tags() + { + var newData = GenerateData("id"); + + A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), + A>.That.IsSameSequenceAs("id2_1", "id2_2", "id4"), + A>.That.IsEmpty())) + .Returns(new Dictionary + { + ["id2_2"] = "name2_2", + ["id2_1"] = "name2_1", + ["id4"] = "name4" + }); + + await tagService.NormalizeAsync(appId, schemaId, schema, newData, null); + + Assert.Equal(JsonValue.Array("name2_1", "name2_2"), newData["tags2"]!["iv"]); + Assert.Equal(JsonValue.Array("name4"), GetNestedTags(newData)); + } + + private static IJsonValue GetNestedTags(NamedContentData newData) + { + var array = (JsonArray)newData["array"]!["iv"]; + var arrayItem = (JsonObject)array[0]; + + return arrayItem["nestedTags2"]; + } + + private static NamedContentData GenerateData(string prefix) + { + return new NamedContentData() + .AddField("tags1", + new ContentFieldData() + .AddValue("iv", JsonValue.Array($"{prefix}1"))) + .AddField("tags2", + new ContentFieldData() + .AddValue("iv", JsonValue.Array($"{prefix}2_1", $"{prefix}2_2"))) + .AddField("string", + new ContentFieldData() + .AddValue("iv", $"{prefix}stringValue")) + .AddField("array", + new ContentFieldData() + .AddValue("iv", + JsonValue.Array( + JsonValue.Object() + .Add("nestedTags1", JsonValue.Array($"{prefix}3")) + .Add("nestedTags2", JsonValue.Array($"{prefix}4")) + .Add("string", $"{prefix}nestedStringValue")))); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs new file mode 100644 index 000000000..fa5a34669 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs @@ -0,0 +1,125 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public class ArrayFieldTests + { + private readonly List errors = new List(); + + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new ArrayFieldProperties()); + + Assert.Equal("my-array", sut.Name); + } + + [Fact] + public async Task Should_not_add_error_if_items_are_valid() + { + var sut = Field(new ArrayFieldProperties()); + + await sut.ValidateAsync(CreateValue(JsonValue.Object()), errors, ValidationTestExtensions.ValidContext); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_items_are_null_and_valid() + { + var sut = Field(new ArrayFieldProperties()); + + await sut.ValidateAsync(CreateValue(null), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_number_of_items_is_equal_to_min_and_max_items() + { + var sut = Field(new ArrayFieldProperties { MinItems = 2, MaxItems = 2 }); + + await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_items_are_required_and_null() + { + var sut = Field(new ArrayFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(null), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_items_are_required_and_empty() + { + var sut = Field(new ArrayFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var sut = Field(new ArrayFieldProperties()); + + await sut.ValidateAsync(JsonValue.Create("invalid"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Not a valid value." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var sut = Field(new ArrayFieldProperties { MinItems = 3 }); + + await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 3 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var sut = Field(new ArrayFieldProperties { MaxItems = 1 }); + + await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } + + private static IJsonValue CreateValue(params JsonObject[]? ids) + { + return ids == null ? JsonValue.Null : JsonValue.Array(ids.OfType().ToArray()); + } + + private static RootField Field(ArrayFieldProperties properties) + { + return Fields.Array(1, "my-array", Partitioning.Invariant, properties); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs new file mode 100644 index 000000000..4e05cdcee --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs @@ -0,0 +1,321 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public class AssetsFieldTests + { + private readonly List errors = new List(); + + public sealed class AssetInfo : IAssetInfo + { + public Guid AssetId { get; set; } + + public string FileName { get; set; } + + public string FileHash { get; set; } + + public string Slug { get; set; } + + public long FileSize { get; set; } + + public bool IsImage { get; set; } + + public int? PixelWidth { get; set; } + + public int? PixelHeight { get; set; } + } + + private readonly AssetInfo document = new AssetInfo + { + AssetId = Guid.NewGuid(), + FileName = "MyDocument.pdf", + FileSize = 1024 * 4, + IsImage = false, + PixelWidth = null, + PixelHeight = null + }; + + private readonly AssetInfo image1 = new AssetInfo + { + AssetId = Guid.NewGuid(), + FileName = "MyImage.png", + FileSize = 1024 * 8, + IsImage = true, + PixelWidth = 800, + PixelHeight = 600 + }; + + private readonly AssetInfo image2 = new AssetInfo + { + AssetId = Guid.NewGuid(), + FileName = "MyImage.png", + FileSize = 1024 * 8, + IsImage = true, + PixelWidth = 800, + PixelHeight = 600 + }; + + private readonly ValidationContext ctx; + + public AssetsFieldTests() + { + ctx = ValidationTestExtensions.Assets(image1, image2, document); + } + + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new AssetsFieldProperties()); + + Assert.Equal("my-assets", sut.Name); + } + + [Fact] + public async Task Should_not_add_error_if_assets_are_valid() + { + var sut = Field(new AssetsFieldProperties()); + + await sut.ValidateAsync(CreateValue(document.AssetId), errors, ctx); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_assets_are_null_and_valid() + { + var sut = Field(new AssetsFieldProperties()); + + await sut.ValidateAsync(CreateValue(null), errors, ctx); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_number_of_assets_is_equal_to_min_and_max_items() + { + var sut = Field(new AssetsFieldProperties { MinItems = 2, MaxItems = 2 }); + + await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_duplicate_values_are_ignored() + { + var sut = Field(new AssetsFieldProperties { AllowDuplicates = true }); + + await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_assets_are_required_and_null() + { + var sut = Field(new AssetsFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(null), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_assets_are_required_and_empty() + { + var sut = Field(new AssetsFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var sut = Field(new AssetsFieldProperties()); + + await sut.ValidateAsync(JsonValue.Create("invalid"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Not a valid value." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var sut = Field(new AssetsFieldProperties { MinItems = 3 }); + + await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 3 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var sut = Field(new AssetsFieldProperties { MaxItems = 1 }); + + await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_asset_are_not_valid() + { + var assetId = Guid.NewGuid(); + + var sut = Field(new AssetsFieldProperties()); + + await sut.ValidateAsync(CreateValue(assetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { $"[1]: Id '{assetId}' not found." }); + } + + [Fact] + public async Task Should_add_error_if_document_is_too_small() + { + var sut = Field(new AssetsFieldProperties { MinSize = 5 * 1024 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[1]: \'4 kB\' less than minimum of \'5 kB\'." }); + } + + [Fact] + public async Task Should_add_error_if_document_is_too_big() + { + var sut = Field(new AssetsFieldProperties { MaxSize = 5 * 1024 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[2]: \'8 kB\' greater than maximum of \'5 kB\'." }); + } + + [Fact] + public async Task Should_add_error_if_document_is_not_an_image() + { + var sut = Field(new AssetsFieldProperties { MustBeImage = true }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[1]: Not an image." }); + } + + [Fact] + public async Task Should_add_error_if_values_contains_duplicate() + { + var sut = Field(new AssetsFieldProperties { MustBeImage = true }); + + await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "Must not contain duplicate values." }); + } + + [Fact] + public async Task Should_add_error_if_image_width_is_too_small() + { + var sut = Field(new AssetsFieldProperties { MinWidth = 1000 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Width \'800px\' less than minimum of \'1000px\'." }); + } + + [Fact] + public async Task Should_add_error_if_image_width_is_too_big() + { + var sut = Field(new AssetsFieldProperties { MaxWidth = 700 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Width \'800px\' greater than maximum of \'700px\'." }); + } + + [Fact] + public async Task Should_add_error_if_image_height_is_too_small() + { + var sut = Field(new AssetsFieldProperties { MinHeight = 800 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Height \'600px\' less than minimum of \'800px\'." }); + } + + [Fact] + public async Task Should_add_error_if_image_height_is_too_big() + { + var sut = Field(new AssetsFieldProperties { MaxHeight = 500 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Height \'600px\' greater than maximum of \'500px\'." }); + } + + [Fact] + public async Task Should_add_error_if_image_has_invalid_aspect_ratio() + { + var sut = Field(new AssetsFieldProperties { AspectWidth = 1, AspectHeight = 1 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Aspect ratio not '1:1'." }); + } + + [Fact] + public async Task Should_add_error_if_image_has_invalid_extension() + { + var sut = Field(new AssetsFieldProperties { AllowedExtensions = ReadOnlyCollection.Create("mp4") }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] + { + "[1]: Invalid file extension.", + "[2]: Invalid file extension." + }); + } + + private static IJsonValue CreateValue(params Guid[]? ids) + { + return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); + } + + private static RootField Field(AssetsFieldProperties properties) + { + return Fields.Assets(1, "my-assets", Partitioning.Invariant, properties); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs new file mode 100644 index 000000000..9988da835 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs @@ -0,0 +1,192 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public class ReferencesFieldTests + { + private readonly List errors = new List(); + private readonly Guid schemaId = Guid.NewGuid(); + private readonly Guid ref1 = Guid.NewGuid(); + private readonly Guid ref2 = Guid.NewGuid(); + + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new ReferencesFieldProperties()); + + Assert.Equal("my-refs", sut.Name); + } + + [Fact] + public async Task Should_not_add_error_if_references_are_valid() + { + var sut = Field(new ReferencesFieldProperties()); + + await sut.ValidateAsync(CreateValue(ref1), errors, Context()); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_references_are_null_and_valid() + { + var sut = Field(new ReferencesFieldProperties()); + + await sut.ValidateAsync(CreateValue(null), errors, Context()); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_number_of_references_is_equal_to_min_and_max_items() + { + var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2 }); + + await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_duplicate_values_are_allowed() + { + var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2, AllowDuplicates = true }); + + await sut.ValidateAsync(CreateValue(ref1, ref1), errors, Context()); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_not_defined() + { + var sut = Field(new ReferencesFieldProperties()); + + await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References((Guid.NewGuid(), ref1))); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_references_are_required_and_null() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); + + await sut.ValidateAsync(CreateValue(null), errors, Context()); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_references_are_required_and_empty() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); + + await sut.ValidateAsync(CreateValue(), errors, Context()); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var sut = Field(new ReferencesFieldProperties()); + + await sut.ValidateAsync(JsonValue.Create("invalid"), errors, Context()); + + errors.Should().BeEquivalentTo( + new[] { "Not a valid value." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MinItems = 3 }); + + await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 3 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MaxItems = 1 }); + + await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_reference_are_not_valid() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); + + await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References()); + + errors.Should().BeEquivalentTo( + new[] { $"Contains invalid reference '{ref1}'." }); + } + + [Fact] + public async Task Should_add_error_if_reference_schema_is_not_valid() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); + + await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References((Guid.NewGuid(), ref1))); + + errors.Should().BeEquivalentTo( + new[] { $"Contains reference '{ref1}' to invalid schema." }); + } + + [Fact] + public async Task Should_add_error_if_reference_contains_duplicate_values() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); + + await sut.ValidateAsync(CreateValue(ref1, ref1), errors, + ValidationTestExtensions.References( + (schemaId, ref1))); + + errors.Should().BeEquivalentTo( + new[] { "Must not contain duplicate values." }); + } + + private static IJsonValue CreateValue(params Guid[]? ids) + { + return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); + } + + private ValidationContext Context() + { + return ValidationTestExtensions.References( + (schemaId, ref1), + (schemaId, ref2)); + } + + private static RootField Field(ReferencesFieldProperties properties) + { + return Fields.References(1, "my-refs", Partitioning.Invariant, properties); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs new file mode 100644 index 000000000..d05115cb8 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs @@ -0,0 +1,139 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public class StringFieldTests + { + private readonly List errors = new List(); + + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new StringFieldProperties()); + + Assert.Equal("my-string", sut.Name); + } + + [Fact] + public async Task Should_not_add_error_if_string_is_valid() + { + var sut = Field(new StringFieldProperties { Label = "" }); + + await sut.ValidateAsync(CreateValue(null), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_string_is_required_but_null() + { + var sut = Field(new StringFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(null), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_string_is_required_but_empty() + { + var sut = Field(new StringFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(string.Empty), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_string_is_shorter_than_min_length() + { + var sut = Field(new StringFieldProperties { MinLength = 10 }); + + await sut.ValidateAsync(CreateValue("123"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 10 character(s)." }); + } + + [Fact] + public async Task Should_add_error_if_string_is_longer_than_max_length() + { + var sut = Field(new StringFieldProperties { MaxLength = 5 }); + + await sut.ValidateAsync(CreateValue("12345678"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 5 character(s)." }); + } + + [Fact] + public async Task Should_add_error_if_string_not_allowed() + { + var sut = Field(new StringFieldProperties { AllowedValues = ReadOnlyCollection.Create("Foo") }); + + await sut.ValidateAsync(CreateValue("Bar"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Not an allowed value." }); + } + + [Fact] + public async Task Should_add_error_if_number_is_not_valid_pattern() + { + var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}" }); + + await sut.ValidateAsync(CreateValue("abc"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Does not match to the pattern." }); + } + + [Fact] + public async Task Should_add_error_if_number_is_not_valid_pattern_with_message() + { + var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}", PatternMessage = "Custom Error Message." }); + + await sut.ValidateAsync(CreateValue("abc"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Custom Error Message." }); + } + + [Fact] + public async Task Should_add_error_if_unique_constraint_failed() + { + var sut = Field(new StringFieldProperties { IsUnique = true }); + + await sut.ValidateAsync(CreateValue("abc"), errors, ValidationTestExtensions.References((Guid.NewGuid(), Guid.NewGuid()))); + + errors.Should().BeEquivalentTo( + new[] { "Another content with the same value exists." }); + } + + private static IJsonValue CreateValue(string? v) + { + return JsonValue.Create(v); + } + + private static RootField Field(StringFieldProperties properties) + { + return Fields.String(1, "my-string", Partitioning.Invariant, properties); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs new file mode 100644 index 000000000..d69d1993d --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs @@ -0,0 +1,159 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public class TagsFieldTests + { + private readonly List errors = new List(); + + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new TagsFieldProperties()); + + Assert.Equal("my-tags", sut.Name); + } + + [Fact] + public async Task Should_not_add_error_if_tags_are_valid() + { + var sut = Field(new TagsFieldProperties()); + + await sut.ValidateAsync(CreateValue("tag"), errors, ValidationTestExtensions.ValidContext); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_tags_are_null_and_valid() + { + var sut = Field(new TagsFieldProperties()); + + await sut.ValidateAsync(CreateValue(null), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_number_of_tags_is_equal_to_min_and_max_items() + { + var sut = Field(new TagsFieldProperties { MinItems = 2, MaxItems = 2 }); + + await sut.ValidateAsync(CreateValue("tag1", "tag2"), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_tags_are_required_but_null() + { + var sut = Field(new TagsFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(null), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_tags_are_required_but_empty() + { + var sut = Field(new TagsFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_tag_value_is_null() + { + var sut = Field(new TagsFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(JsonValue.Array(JsonValue.Null), errors); + + errors.Should().BeEquivalentTo( + new[] { "[1]: Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_tag_value_is_empty() + { + var sut = Field(new TagsFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(string.Empty), errors); + + errors.Should().BeEquivalentTo( + new[] { "[1]: Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var sut = Field(new TagsFieldProperties()); + + await sut.ValidateAsync(JsonValue.Create("invalid"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Not a valid value." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var sut = Field(new TagsFieldProperties { MinItems = 3 }); + + await sut.ValidateAsync(CreateValue("tag-1", "tag-2"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 3 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var sut = Field(new TagsFieldProperties { MaxItems = 1 }); + + await sut.ValidateAsync(CreateValue("tag-1", "tag-2"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_contains_an_not_allowed_values() + { + var sut = Field(new TagsFieldProperties { AllowedValues = ReadOnlyCollection.Create("tag-2", "tag-3") }); + + await sut.ValidateAsync(CreateValue("tag-1", "tag-2", null), errors); + + errors.Should().BeEquivalentTo( + new[] { "[1]: Not an allowed value." }); + } + + private static IJsonValue CreateValue(params string?[]? ids) + { + return ids == null ? JsonValue.Null : JsonValue.Array(ids.OfType().ToArray()); + } + + private static RootField Field(TagsFieldProperties properties) + { + return Fields.Tags(1, "my-tags", Partitioning.Invariant, properties); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs new file mode 100644 index 000000000..2977b6acb --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs @@ -0,0 +1,129 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +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.ValidateContent; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public class UIFieldTests + { + private readonly List errors = new List(); + + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new UIFieldProperties()); + + Assert.Equal("my-ui", sut.Name); + } + + [Fact] + public async Task Should_not_add_error_if_value_is_undefined() + { + var sut = Field(new UIFieldProperties()); + + await sut.ValidateAsync(Undefined.Value, errors, ValidationTestExtensions.ValidContext); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_value_is_json_null() + { + var sut = Field(new UIFieldProperties()); + + await sut.ValidateAsync(JsonValue.Null, errors); + + errors.Should().BeEquivalentTo( + new[] { "Value must not be defined." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_valid() + { + var sut = Field(new UIFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(JsonValue.True, errors); + + errors.Should().BeEquivalentTo( + new[] { "Value must not be defined." }); + } + + [Fact] + public async Task Should_add_error_if_field_object_is_defined() + { + var schema = + new Schema("my-schema") + .AddUI(1, "my-ui1", Partitioning.Invariant) + .AddUI(2, "my-ui2", Partitioning.Invariant); + + var data = + new NamedContentData() + .AddField("my-ui1", new ContentFieldData()) + .AddField("my-ui2", new ContentFieldData() + .AddValue("iv", null)); + + var validationContext = ValidationTestExtensions.ValidContext; + var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext); + + await validator.ValidateAsync(data); + + validator.Errors.Should().BeEquivalentTo( + new[] + { + new ValidationError("Value must not be defined.", "my-ui1"), + new ValidationError("Value must not be defined.", "my-ui2") + }); + } + + [Fact] + public async Task Should_add_error_if_array_item_field_is_defined() + { + var schema = + new Schema("my-schema") + .AddArray(1, "my-array", Partitioning.Invariant, array => array + .AddUI(101, "my-ui")); + + var data = + new NamedContentData() + .AddField("my-array", new ContentFieldData() + .AddValue("iv", + JsonValue.Array( + JsonValue.Object() + .Add("my-ui", null)))); + + var validationContext = + new ValidationContext( + Guid.NewGuid(), + Guid.NewGuid(), + (c, s) => null!, + (s) => null!, + (c) => null!); + + var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext); + + await validator.ValidateAsync(data); + + validator.Errors.Should().BeEquivalentTo( + new[] { new ValidationError("Value must not be defined.", "my-array[1].my-ui") }); + } + + private static NestedField Field(UIFieldProperties properties) + { + return new NestedField(1, "my-ui", properties); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs new file mode 100644 index 000000000..ecd4a74dd --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs @@ -0,0 +1,86 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public static class ValidationTestExtensions + { + private static readonly Task> EmptyReferences = Task.FromResult>(new List<(Guid SchemaId, Guid Id)>()); + private static readonly Task> EmptyAssets = Task.FromResult>(new List()); + + public static readonly ValidationContext ValidContext = new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), + (x, y) => EmptyReferences, + (x) => EmptyReferences, + (x) => EmptyAssets); + + public static Task ValidateAsync(this IValidator validator, object? value, IList errors, ValidationContext? context = null) + { + return validator.ValidateAsync(value, + CreateContext(context), + CreateFormatter(errors)); + } + + public static Task ValidateOptionalAsync(this IValidator validator, object? value, IList errors, ValidationContext? context = null) + { + return validator.ValidateAsync( + value, + CreateContext(context).Optional(true), + CreateFormatter(errors)); + } + + public static Task ValidateAsync(this IField field, object? value, IList errors, ValidationContext? context = null) + { + return new FieldValidator(FieldValueValidatorsFactory.CreateValidators(field).ToArray(), field) + .ValidateAsync( + value, + CreateContext(context), + CreateFormatter(errors)); + } + + private static AddError CreateFormatter(IList errors) + { + return (field, message) => + { + if (field == null || !field.Any()) + { + errors.Add(message); + } + else + { + errors.Add($"{field.ToPathString()}: {message}"); + } + }; + } + + private static ValidationContext CreateContext(ValidationContext? context) + { + return context ?? ValidContext; + } + + public static ValidationContext Assets(params IAssetInfo[] assets) + { + var actual = Task.FromResult>(assets.ToList()); + + return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => EmptyReferences, x => EmptyReferences, x => actual); + } + + public static ValidationContext References(params (Guid Id, Guid SchemaId)[] referencesIds) + { + var actual = Task.FromResult>(referencesIds.ToList()); + + return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => actual, x => actual, x => EmptyAssets); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj new file mode 100644 index 000000000..a0012f39f --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -0,0 +1,33 @@ + + + Exe + netcoreapp3.0 + Squidex.Domain.Apps.Core + 8.0 + enable + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + ..\..\Squidex.ruleset + + + + + diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs new file mode 100644 index 000000000..aa535de60 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs @@ -0,0 +1,173 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Squidex.Domain.Apps.Core.Apps.Json; +using Squidex.Domain.Apps.Core.Contents.Json; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules.Json; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Schemas.Json; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Newtonsoft; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.Reflection; +using Xunit; + +namespace Squidex.Domain.Apps.Core +{ + public static class TestUtils + { + public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); + + public static IJsonSerializer CreateSerializer(TypeNameHandling typeNameHandling = TypeNameHandling.Auto) + { + var typeNameRegistry = + new TypeNameRegistry() + .Map(new FieldRegistry()) + .Map(new RuleRegistry()) + .MapUnmapped(typeof(TestUtils).Assembly); + + var serializerSettings = new JsonSerializerSettings + { + SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry), + + ContractResolver = new ConverterContractResolver( + new AppClientsConverter(), + new AppContributorsConverter(), + new AppPatternsConverter(), + new ClaimsPrincipalConverter(), + new ContentFieldDataConverter(), + new EnvelopeHeadersConverter(), + new FilterConverter(), + new InstantConverter(), + new JsonValueConverter(), + new LanguageConverter(), + new LanguagesConfigConverter(), + new NamedGuidIdConverter(), + new NamedLongIdConverter(), + new NamedStringIdConverter(), + new PropertyPathConverter(), + new RefTokenConverter(), + new RolesConverter(), + new RuleConverter(), + new SchemaConverter(), + new StatusConverter(), + new StringEnumConverter(), + new WorkflowConverter(), + new WorkflowTransitionConverter()), + + TypeNameHandling = typeNameHandling + }; + + return new NewtonsoftJsonSerializer(serializerSettings); + } + + public static Schema MixedSchema(bool isSingleton = false) + { + var schema = new Schema("user", isSingleton: isSingleton) + .Publish() + .AddArray(101, "root-array", Partitioning.Language, f => f + .AddAssets(201, "nested-assets") + .AddBoolean(202, "nested-boolean") + .AddDateTime(203, "nested-datetime") + .AddGeolocation(204, "nested-geolocation") + .AddJson(205, "nested-json") + .AddJson(211, "nested-json2") + .AddNumber(206, "nested-number") + .AddReferences(207, "nested-references") + .AddString(208, "nested-string") + .AddTags(209, "nested-tags") + .AddUI(210, "nested-ui")) + .AddAssets(102, "root-assets", Partitioning.Invariant, + new AssetsFieldProperties()) + .AddBoolean(103, "root-boolean", Partitioning.Invariant, + new BooleanFieldProperties()) + .AddDateTime(104, "root-datetime", Partitioning.Invariant, + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime }) + .AddDateTime(105, "root-date", Partitioning.Invariant, + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date }) + .AddGeolocation(106, "root-geolocation", Partitioning.Invariant, + new GeolocationFieldProperties()) + .AddJson(107, "root-json", Partitioning.Invariant, + new JsonFieldProperties()) + .AddNumber(108, "root-number", Partitioning.Invariant, + new NumberFieldProperties { MinValue = 1, MaxValue = 10 }) + .AddReferences(109, "root-references", Partitioning.Invariant, + new ReferencesFieldProperties()) + .AddString(110, "root-string1", Partitioning.Invariant, + new StringFieldProperties { Label = "My String1", IsRequired = true, AllowedValues = ReadOnlyCollection.Create("a", "b") }) + .AddString(111, "root-string2", Partitioning.Invariant, + new StringFieldProperties { Hints = "My String1" }) + .AddTags(112, "root-tags", Partitioning.Language, + new TagsFieldProperties()) + .AddUI(113, "root-ui", Partitioning.Language, + new UIFieldProperties()) + .Update(new SchemaProperties { Hints = "The User" }) + .HideField(104) + .HideField(211, 101) + .DisableField(109) + .DisableField(212, 101) + .LockField(105); + + return schema; + } + + public static T SerializeAndDeserialize(this T value) + { + return DefaultSerializer.Deserialize(DefaultSerializer.Serialize(value)); + } + + public static void TestFreeze(IFreezable sut) + { + var properties = + sut.GetType().GetRuntimeProperties() + .Where(x => + x.CanWrite && + x.CanRead && + x.Name != "IsFrozen"); + + foreach (var property in properties) + { + var value = + property.PropertyType.IsValueType ? Activator.CreateInstance(property.PropertyType) : null; + + property.SetValue(sut, value); + + var result = property.GetValue(sut); + + Assert.Equal(value, result); + } + + sut.Freeze(); + + foreach (var property in properties) + { + var value = + property.PropertyType.IsValueType ? Activator.CreateInstance(property.PropertyType) : null; + + Assert.Throws(() => + { + try + { + property.SetValue(sut, value); + } + catch (Exception ex) when (ex.InnerException != null) + { + throw ex.InnerException; + } + }); + } + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs new file mode 100644 index 000000000..977e0fde9 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Validation; +using Xunit; + +#pragma warning disable IDE0067 // Dispose objects before losing scope + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppCommandMiddlewareTests : HandlerTestBase + { + private readonly IContextProvider contextProvider = A.Fake(); + private readonly IAssetStore assetStore = A.Fake(); + private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + private readonly Context requestContext = Context.Anonymous(); + private readonly AppCommandMiddleware sut; + + public sealed class MyCommand : SquidexCommand + { + } + + protected override Guid Id + { + get { return appId; } + } + + public AppCommandMiddlewareTests() + { + A.CallTo(() => contextProvider.Context) + .Returns(requestContext); + + sut = new AppCommandMiddleware(A.Fake(), assetStore, assetThumbnailGenerator, contextProvider); + } + + [Fact] + public async Task Should_replace_context_app_with_grain_result() + { + var result = A.Fake(); + + var command = CreateCommand(new MyCommand()); + var context = CreateContextForCommand(command); + + context.Complete(result); + + await sut.HandleAsync(context); + + Assert.Same(result, requestContext.App); + } + + [Fact] + public async Task Should_upload_image_to_store() + { + var stream = new MemoryStream(); + + var file = new AssetFile("name.jpg", "image/jpg", 1024, () => stream); + + var command = CreateCommand(new UploadAppImage { AppId = appId, File = file }); + var context = CreateContextForCommand(command); + + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .Returns(new ImageInfo(100, 100)); + + await sut.HandleAsync(context); + + A.CallTo(() => assetStore.UploadAsync(appId.ToString(), stream, true, A.Ignored)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_throw_exception_when_file_to_upload_is_not_an_image() + { + var stream = new MemoryStream(); + + var file = new AssetFile("name.jpg", "image/jpg", 1024, () => stream); + + var command = CreateCommand(new UploadAppImage { AppId = appId, File = file }); + var context = CreateContextForCommand(command); + + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .Returns(Task.FromResult(null)); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs new file mode 100644 index 000000000..01adb340b --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs @@ -0,0 +1,659 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppGrainTests : HandlerTestBase + { + private readonly IAppPlansProvider appPlansProvider = A.Fake(); + private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); + private readonly IUser user = A.Fake(); + private readonly IUserResolver userResolver = A.Fake(); + private readonly string contributorId = Guid.NewGuid().ToString(); + private readonly string clientId = "client"; + private readonly string clientNewName = "My Client"; + private readonly string roleName = "My Role"; + private readonly string planIdPaid = "premium"; + private readonly string planIdFree = "free"; + private readonly AppGrain sut; + private readonly Guid workflowId = Guid.NewGuid(); + private readonly Guid patternId1 = Guid.NewGuid(); + private readonly Guid patternId2 = Guid.NewGuid(); + private readonly Guid patternId3 = Guid.NewGuid(); + private readonly InitialPatterns initialPatterns; + + protected override Guid Id + { + get { return AppId; } + } + + public AppGrainTests() + { + A.CallTo(() => user.Id) + .Returns(contributorId); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId)) + .Returns(user); + + A.CallTo(() => appPlansProvider.GetPlan(A.Ignored)) + .Returns(new ConfigAppLimitsPlan { MaxContributors = 10 }); + + initialPatterns = new InitialPatterns + { + { patternId1, new AppPattern("Number", "[0-9]") }, + { patternId2, new AppPattern("Numbers", "[0-9]*") } + }; + + sut = new AppGrain(initialPatterns, Store, A.Dummy(), appPlansProvider, appPlansBillingManager, userResolver); + sut.ActivateAsync(Id).Wait(); + } + + [Fact] + public async Task Command_should_throw_exception_if_app_is_archived() + { + await ExecuteCreateAsync(); + await ExecuteArchiveAsync(); + + await Assert.ThrowsAsync(ExecuteAttachClientAsync); + } + + [Fact] + public async Task Create_should_create_events_and_update_state() + { + var command = new CreateApp { Name = AppName, Actor = Actor, AppId = AppId }; + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(AppName, sut.Snapshot.Name); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppCreated { Name = AppName }), + CreateEvent(new AppContributorAssigned { ContributorId = Actor.Identifier, Role = Role.Owner }), + CreateEvent(new AppLanguageAdded { Language = Language.EN }), + CreateEvent(new AppPatternAdded { PatternId = patternId1, Name = "Number", Pattern = "[0-9]" }), + CreateEvent(new AppPatternAdded { PatternId = patternId2, Name = "Numbers", Pattern = "[0-9]*" }) + ); + } + + [Fact] + public async Task Update_should_create_events_and_update_state() + { + var command = new UpdateApp { Label = "my-label", Description = "my-description" }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal("my-label", sut.Snapshot.Label); + Assert.Equal("my-description", sut.Snapshot.Description); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppUpdated { Label = "my-label", Description = "my-description" }) + ); + } + + [Fact] + public async Task UploadImage_should_create_events_and_update_state() + { + var command = new UploadAppImage { File = new AssetFile("image.png", "image/png", 100, () => new MemoryStream()) }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal("image/png", sut.Snapshot.Image!.MimeType); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppImageUploaded { Image = sut.Snapshot.Image }) + ); + } + + [Fact] + public async Task RemoveImage_should_create_events_and_update_state() + { + var command = new RemoveAppImage(); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Null(sut.Snapshot.Image); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppImageRemoved()) + ); + } + + [Fact] + public async Task ChangePlan_should_create_events_and_update_state() + { + var command = new ChangePlan { PlanId = planIdPaid }; + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) + .Returns(new PlanChangedResult()); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + Assert.True(result.Value is PlanChangedResult); + + Assert.Equal(planIdPaid, sut.Snapshot.Plan!.PlanId); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPlanChanged { PlanId = planIdPaid }) + ); + } + + [Fact] + public async Task ChangePlan_should_reset_plan_for_reset_plan() + { + var command = new ChangePlan { PlanId = planIdFree }; + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) + .Returns(new PlanChangedResult()); + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdFree)) + .Returns(new PlanResetResult()); + + await ExecuteCreateAsync(); + await ExecuteChangePlanAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + Assert.True(result.Value is PlanResetResult); + + Assert.Null(sut.Snapshot.Plan); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPlanReset()) + ); + } + + [Fact] + public async Task ChangePlan_should_not_make_update_for_redirect_result() + { + var command = new ChangePlan { PlanId = planIdPaid }; + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) + .Returns(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); + + Assert.Null(sut.Snapshot.Plan); + } + + [Fact] + public async Task ChangePlan_should_not_call_billing_manager_for_callback() + { + var command = new ChangePlan { PlanId = planIdPaid, FromCallback = true }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(5)); + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task AssignContributor_should_create_events_and_update_state() + { + var command = new AssignContributor { ContributorId = contributorId, Role = Role.Editor }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Role.Editor, sut.Snapshot.Contributors[contributorId]); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Editor, IsAdded = true }) + ); + } + + [Fact] + public async Task AssignContributor_should_create_update_events_and_update_state() + { + var command = new AssignContributor { ContributorId = contributorId, Role = Role.Owner }; + + await ExecuteCreateAsync(); + await ExecuteAssignContributorAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Role.Owner, sut.Snapshot.Contributors[contributorId]); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Owner }) + ); + } + + [Fact] + public async Task RemoveContributor_should_create_events_and_update_state() + { + var command = new RemoveContributor { ContributorId = contributorId }; + + await ExecuteCreateAsync(); + await ExecuteAssignContributorAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) + ); + } + + [Fact] + public async Task AttachClient_should_create_events_and_update_state() + { + var command = new AttachClient { Id = clientId }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.True(sut.Snapshot.Clients.ContainsKey(clientId)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) + ); + } + + [Fact] + public async Task UpdateClient_should_create_events_and_update_state() + { + var command = new UpdateClient { Id = clientId, Name = clientNewName, Role = Role.Developer }; + + await ExecuteCreateAsync(); + await ExecuteAttachClientAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), + CreateEvent(new AppClientUpdated { Id = clientId, Role = Role.Developer }) + ); + } + + [Fact] + public async Task RevokeClient_should_create_events_and_update_state() + { + var command = new RevokeClient { Id = clientId }; + + await ExecuteCreateAsync(); + await ExecuteAttachClientAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.False(sut.Snapshot.Clients.ContainsKey(clientId)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppClientRevoked { Id = clientId }) + ); + } + + [Fact] + public async Task AddWorkflow_should_create_events_and_update_state() + { + var command = new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.NotEmpty(sut.Snapshot.Workflows); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppWorkflowAdded { WorkflowId = workflowId, Name = "my-workflow" }) + ); + } + + [Fact] + public async Task UpdateWorkflow_should_create_events_and_update_state() + { + var command = new UpdateWorkflow { WorkflowId = workflowId, Workflow = Workflow.Default }; + + await ExecuteCreateAsync(); + await ExecuteAddWorkflowAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.NotEmpty(sut.Snapshot.Workflows); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppWorkflowUpdated { WorkflowId = workflowId, Workflow = Workflow.Default }) + ); + } + + [Fact] + public async Task DeleteWorkflow_should_create_events_and_update_state() + { + var command = new DeleteWorkflow { WorkflowId = workflowId }; + + await ExecuteCreateAsync(); + await ExecuteAddWorkflowAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Empty(sut.Snapshot.Workflows); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppWorkflowDeleted { WorkflowId = workflowId }) + ); + } + + [Fact] + public async Task AddLanguage_should_create_events_and_update_state() + { + var command = new AddLanguage { Language = Language.DE }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageAdded { Language = Language.DE }) + ); + } + + [Fact] + public async Task RemoveLanguage_should_create_events_and_update_state() + { + var command = new RemoveLanguage { Language = Language.DE }; + + await ExecuteCreateAsync(); + await ExecuteAddLanguageAsync(Language.DE); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.False(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageRemoved { Language = Language.DE }) + ); + } + + [Fact] + public async Task UpdateLanguage_should_create_events_and_update_state() + { + var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } }; + + await ExecuteCreateAsync(); + await ExecuteAddLanguageAsync(Language.DE); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List { Language.EN } }) + ); + } + + [Fact] + public async Task AddRole_should_create_events_and_update_state() + { + var command = new AddRole { Name = roleName }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(1, sut.Snapshot.Roles.CustomCount); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppRoleAdded { Name = roleName }) + ); + } + + [Fact] + public async Task DeleteRole_should_create_events_and_update_state() + { + var command = new DeleteRole { Name = roleName }; + + await ExecuteCreateAsync(); + await ExecuteAddRoleAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(0, sut.Snapshot.Roles.CustomCount); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppRoleDeleted { Name = roleName }) + ); + } + + [Fact] + public async Task UpdateRole_should_create_events_and_update_state() + { + var command = new UpdateRole { Name = roleName, Permissions = new[] { "clients.read" } }; + + await ExecuteCreateAsync(); + await ExecuteAddRoleAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppRoleUpdated { Name = roleName, Permissions = new[] { "clients.read" } }) + ); + } + + [Fact] + public async Task AddPattern_should_create_events_and_update_state() + { + var command = new AddPattern { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(initialPatterns.Count + 1, sut.Snapshot.Patterns.Count); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPatternAdded { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }) + ); + } + + [Fact] + public async Task DeletePattern_should_create_events_and_update_state() + { + var command = new DeletePattern { PatternId = patternId3 }; + + await ExecuteCreateAsync(); + await ExecuteAddPatternAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(initialPatterns.Count, sut.Snapshot.Patterns.Count); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPatternDeleted { PatternId = patternId3 }) + ); + } + + [Fact] + public async Task UpdatePattern_should_create_events_and_update_state() + { + var command = new UpdatePattern { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }; + + await ExecuteCreateAsync(); + await ExecuteAddPatternAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPatternUpdated { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }) + ); + } + + [Fact] + public async Task ArchiveApp_should_create_events_and_update_state() + { + var command = new ArchiveApp(); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(5)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppArchived()) + ); + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, AppNamedId, null)) + .MustHaveHappened(); + } + + private Task ExecuteAddPatternAsync() + { + return sut.ExecuteAsync(CreateCommand(new AddPattern { PatternId = patternId3, Name = "Name", Pattern = ".*" })); + } + + private Task ExecuteCreateAsync() + { + return sut.ExecuteAsync(CreateCommand(new CreateApp { Name = AppName })); + } + + private Task ExecuteAssignContributorAsync() + { + return sut.ExecuteAsync(CreateCommand(new AssignContributor { ContributorId = contributorId, Role = Role.Editor })); + } + + private Task ExecuteAttachClientAsync() + { + return sut.ExecuteAsync(CreateCommand(new AttachClient { Id = clientId })); + } + + private Task ExecuteAddRoleAsync() + { + return sut.ExecuteAsync(CreateCommand(new AddRole { Name = roleName })); + } + + private Task ExecuteAddLanguageAsync(Language language) + { + return sut.ExecuteAsync(CreateCommand(new AddLanguage { Language = language })); + } + + private Task ExecuteAddWorkflowAsync() + { + return sut.ExecuteAsync(CreateCommand(new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" })); + } + + private Task ExecuteChangePlanAsync() + { + return sut.ExecuteAsync(CreateCommand(new ChangePlan { PlanId = planIdPaid })); + } + + private Task ExecuteArchiveAsync() + { + return sut.ExecuteAsync(CreateCommand(new ArchiveApp())); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs new file mode 100644 index 000000000..ffd95eb48 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Billing +{ + public class NoopAppPlanBillingManagerTests + { + private readonly NoopAppPlanBillingManager sut = new NoopAppPlanBillingManager(); + + [Fact] + public void Should_not_have_portal() + { + Assert.False(sut.HasPortal); + } + + [Fact] + public async Task Should_do_nothing_when_changing_plan() + { + await sut.ChangePlanAsync(null!, null!, null); + } + + [Fact] + public async Task Should_not_return_portal_link() + { + Assert.Equal(string.Empty, await sut.GetPortalLinkAsync(null!)); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs new file mode 100644 index 000000000..3dd7d9ca3 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs @@ -0,0 +1,223 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Squidex.Shared.Users; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public class GuardAppContributorsTests + { + private readonly IUser user1 = A.Fake(); + private readonly IUser user2 = A.Fake(); + private readonly IUser user3 = A.Fake(); + private readonly IUserResolver users = A.Fake(); + private readonly IAppLimitsPlan appPlan = A.Fake(); + private readonly AppContributors contributors_0 = AppContributors.Empty; + private readonly Roles roles = Roles.Empty; + + public GuardAppContributorsTests() + { + A.CallTo(() => user1.Id).Returns("1"); + A.CallTo(() => user2.Id).Returns("2"); + A.CallTo(() => user3.Id).Returns("3"); + + A.CallTo(() => users.FindByIdOrEmailAsync("1")).Returns(user1); + A.CallTo(() => users.FindByIdOrEmailAsync("2")).Returns(user2); + A.CallTo(() => users.FindByIdOrEmailAsync("3")).Returns(user3); + + A.CallTo(() => users.FindByIdOrEmailAsync("1@email.com")).Returns(user1); + A.CallTo(() => users.FindByIdOrEmailAsync("2@email.com")).Returns(user2); + A.CallTo(() => users.FindByIdOrEmailAsync("3@email.com")).Returns(user3); + + A.CallTo(() => users.FindByIdOrEmailAsync("notfound")) + .Returns(Task.FromResult(null)); + + A.CallTo(() => appPlan.MaxContributors) + .Returns(10); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_contributor_id_is_null() + { + var command = new AssignContributor(); + + await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan), + new ValidationError("Contributor id is required.", "ContributorId")); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_role_not_valid() + { + var command = new AssignContributor { ContributorId = "1", Role = "Invalid" }; + + await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan), + new ValidationError("Role is not a valid value.", "Role")); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_user_already_exists_with_same_role() + { + var command = new AssignContributor { ContributorId = "1", Role = Role.Owner }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + + await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan), + new ValidationError("Contributor has already this role.", "Role")); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_some_role_but_is_from_restore() + { + var command = new AssignContributor { ContributorId = "1", Role = Role.Owner, IsRestore = true }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + + await GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_user_not_found() + { + var command = new AssignContributor { ContributorId = "notfound", Role = Role.Owner }; + + await Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan)); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_user_is_actor() + { + var command = new AssignContributor { ContributorId = "3", Role = Role.Editor, Actor = new RefToken("user", "3") }; + + await Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan)); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_contributor_max_reached() + { + A.CallTo(() => appPlan.MaxContributors) + .Returns(2); + + var command = new AssignContributor { ContributorId = "3" }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Editor); + + await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan), + new ValidationError("You have reached the maximum number of contributors for your plan.")); + } + + [Fact] + public async Task CanAssign_assign_if_if_user_added_by_email() + { + var command = new AssignContributor { ContributorId = "1@email.com" }; + + await GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan); + + Assert.Equal("1", command.ContributorId); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_user_found() + { + A.CallTo(() => appPlan.MaxContributors) + .Returns(-1); + + var command = new AssignContributor { ContributorId = "1" }; + + await GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_contributor_has_another_role() + { + var command = new AssignContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", Role.Developer); + + await GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_role_changed() + { + A.CallTo(() => appPlan.MaxContributors) + .Returns(2); + + var command = new AssignContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", Role.Developer); + var contributors_2 = contributors_1.Assign("2", Role.Developer); + + await GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_from_restore() + { + A.CallTo(() => appPlan.MaxContributors) + .Returns(2); + + var command = new AssignContributor { ContributorId = "3", IsRestore = true }; + + var contributors_1 = contributors_0.Assign("1", Role.Editor); + var contributors_2 = contributors_1.Assign("2", Role.Editor); + + await GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_id_is_null() + { + var command = new RemoveContributor(); + + ValidationAssert.Throws(() => GuardAppContributors.CanRemove(contributors_0, command), + new ValidationError("Contributor id is required.", "ContributorId")); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_not_found() + { + var command = new RemoveContributor { ContributorId = "1" }; + + Assert.Throws(() => GuardAppContributors.CanRemove(contributors_0, command)); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_is_only_owner() + { + var command = new RemoveContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Editor); + + ValidationAssert.Throws(() => GuardAppContributors.CanRemove(contributors_2, command), + new ValidationError("Cannot remove the only owner.")); + } + + [Fact] + public void CanRemove_should_not_throw_exception_if_contributor_not_only_owner() + { + var command = new RemoveContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Owner); + + GuardAppContributors.CanRemove(contributors_2, command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs new file mode 100644 index 000000000..ed688e8f4 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs @@ -0,0 +1,165 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public class GuardAppRolesTests + { + private readonly string roleName = "Role1"; + private readonly Roles roles_0 = Roles.Empty; + private readonly AppContributors contributors = AppContributors.Empty; + private readonly AppClients clients = AppClients.Empty; + + [Fact] + public void CanAdd_should_throw_exception_if_name_empty() + { + var command = new AddRole { Name = null! }; + + ValidationAssert.Throws(() => GuardAppRoles.CanAdd(roles_0, command), + new ValidationError("Name is required.", "Name")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_name_exists() + { + var roles_1 = roles_0.Add(roleName); + + var command = new AddRole { Name = roleName }; + + ValidationAssert.Throws(() => GuardAppRoles.CanAdd(roles_1, command), + new ValidationError("A role with the same name already exists.")); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_command_is_valid() + { + var command = new AddRole { Name = roleName }; + + GuardAppRoles.CanAdd(roles_0, command); + } + + [Fact] + public void CanDelete_should_throw_exception_if_name_empty() + { + var command = new DeleteRole { Name = null! }; + + ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_0, command, contributors, clients), + new ValidationError("Name is required.", "Name")); + } + + [Fact] + public void CanDelete_should_throw_exception_if_role_not_found() + { + var command = new DeleteRole { Name = roleName }; + + Assert.Throws(() => GuardAppRoles.CanDelete(roles_0, command, contributors, clients)); + } + + [Fact] + public void CanDelete_should_throw_exception_if_contributor_found() + { + var roles_1 = roles_0.Add(roleName); + + var command = new DeleteRole { Name = roleName }; + + ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors.Assign("1", roleName), clients), + new ValidationError("Cannot remove a role when a contributor is assigned.")); + } + + [Fact] + public void CanDelete_should_throw_exception_if_client_found() + { + var roles_1 = roles_0.Add(roleName); + + var command = new DeleteRole { Name = roleName }; + + ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors, clients.Add("1", new AppClient("client", "1", roleName))), + new ValidationError("Cannot remove a role when a client is assigned.")); + } + + [Fact] + public void CanDelete_should_throw_exception_if_default_role() + { + var roles_1 = roles_0.Add(Role.Developer); + + var command = new DeleteRole { Name = Role.Developer }; + + ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors, clients), + new ValidationError("Cannot delete a default role.")); + } + + [Fact] + public void CanDelete_should_not_throw_exception_if_command_is_valid() + { + var roles_1 = roles_0.Add(roleName); + + var command = new DeleteRole { Name = roleName }; + + GuardAppRoles.CanDelete(roles_1, command, contributors, clients); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_name_empty() + { + var roles_1 = roles_0.Add(roleName); + + var command = new UpdateRole { Name = null!, Permissions = new[] { "P1" } }; + + ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(roles_1, command), + new ValidationError("Name is required.", "Name")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_permission_is_null() + { + var roles_1 = roles_0.Add(roleName); + + var command = new UpdateRole { Name = roleName, Permissions = null! }; + + ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(roles_1, command), + new ValidationError("Permissions is required.", "Permissions")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_default_role() + { + var roles_1 = roles_0.Add(Role.Developer); + + var command = new UpdateRole { Name = Role.Developer, Permissions = new[] { "P1" } }; + + ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(roles_1, command), + new ValidationError("Cannot update a default role.")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_role_does_not_exists() + { + var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } }; + + Assert.Throws(() => GuardAppRoles.CanUpdate(roles_0, command)); + } + + [Fact] + public void CanUpdate_should_not_throw_exception_if_role_exist_with_valid_command() + { + var roles_1 = roles_0.Add(roleName); + + var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } }; + + GuardAppRoles.CanUpdate(roles_1, command); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs new file mode 100644 index 000000000..a1e2d7605 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs @@ -0,0 +1,132 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Validation; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public class GuardAppTests + { + private readonly IUserResolver users = A.Fake(); + private readonly IAppPlansProvider appPlans = A.Fake(); + private readonly IAppLimitsPlan basicPlan = A.Fake(); + private readonly IAppLimitsPlan freePlan = A.Fake(); + + public GuardAppTests() + { + A.CallTo(() => users.FindByIdOrEmailAsync(A.Ignored)) + .Returns(A.Dummy()); + + A.CallTo(() => appPlans.GetPlan("notfound")) + .Returns(null!); + + A.CallTo(() => appPlans.GetPlan("basic")) + .Returns(basicPlan); + + A.CallTo(() => appPlans.GetPlan("free")) + .Returns(freePlan); + } + + [Fact] + public void CanCreate_should_throw_exception_if_name_not_valid() + { + var command = new CreateApp { Name = "INVALID NAME" }; + + ValidationAssert.Throws(() => GuardApp.CanCreate(command), + new ValidationError("Name is not a valid slug.", "Name")); + } + + [Fact] + public void CanCreate_should_not_throw_exception_if_app_name_is_valid() + { + var command = new CreateApp { Name = "new-app" }; + + GuardApp.CanCreate(command); + } + + [Fact] + public void CanUploadImage_should_throw_exception_if_name_not_valid() + { + var command = new UploadAppImage(); + + ValidationAssert.Throws(() => GuardApp.CanUploadImage(command), + new ValidationError("File is required.", "File")); + } + + [Fact] + public void CanUploadImage_should_not_throw_exception_if_app_name_is_valid() + { + var command = new UploadAppImage { File = new AssetFile("file.png", "image/png", 100, () => new MemoryStream()) }; + + GuardApp.CanUploadImage(command); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_id_is_null() + { + var command = new ChangePlan { Actor = new RefToken("user", "me") }; + + AppPlan? plan = null; + + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), + new ValidationError("Plan id is required.", "PlanId")); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_not_found() + { + var command = new ChangePlan { PlanId = "notfound", Actor = new RefToken("user", "me") }; + + AppPlan? plan = null; + + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), + new ValidationError("A plan with this id does not exist.", "PlanId")); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_was_configured_from_another_user() + { + var command = new ChangePlan { PlanId = "basic", Actor = new RefToken("user", "me") }; + + var plan = new AppPlan(new RefToken("user", "other"), "premium"); + + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), + new ValidationError("Plan can only changed from the user who configured the plan initially.")); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_is_the_same() + { + var command = new ChangePlan { PlanId = "basic", Actor = new RefToken("user", "me") }; + + var plan = new AppPlan(command.Actor, "basic"); + + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), + new ValidationError("App has already this plan.")); + } + + [Fact] + public void CanChangePlan_should_not_throw_exception_if_same_user_but_other_plan() + { + var command = new ChangePlan { PlanId = "basic", Actor = new RefToken("user", "me") }; + + var plan = new AppPlan(command.Actor, "premium"); + + GuardApp.CanChangePlan(command, plan, appPlans); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs new file mode 100644 index 000000000..02cac699d --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs @@ -0,0 +1,213 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public class GuardAppWorkflowTests + { + private readonly Guid workflowId = Guid.NewGuid(); + private readonly Workflows workflows; + + public GuardAppWorkflowTests() + { + workflows = Workflows.Empty.Add(workflowId, "name"); + } + + [Fact] + public void CanAdd_should_throw_exception_if_name_is_not_defined() + { + var command = new AddWorkflow(); + + ValidationAssert.Throws(() => GuardAppWorkflows.CanAdd(command), + new ValidationError("Name is required.", "Name")); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_command_is_valid() + { + var command = new AddWorkflow { Name = "my-workflow" }; + + GuardAppWorkflows.CanAdd(command); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_not_found() + { + var command = new UpdateWorkflow + { + Workflow = Workflow.Empty, + WorkflowId = Guid.NewGuid() + }; + + Assert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command)); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_is_not_defined() + { + var command = new UpdateWorkflow { WorkflowId = workflowId }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), + new ValidationError("Workflow is required.", "Workflow")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_has_no_initial_step() + { + var command = new UpdateWorkflow + { + Workflow = new Workflow( + default, + new Dictionary + { + [Status.Published] = new WorkflowStep() + }), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), + new ValidationError("Initial step is required.", "Workflow.Initial")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_initial_step_is_published() + { + var command = new UpdateWorkflow + { + Workflow = new Workflow( + Status.Published, + new Dictionary + { + [Status.Published] = new WorkflowStep() + }), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), + new ValidationError("Initial step cannot be published step.", "Workflow.Initial")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_does_not_have_published_state() + { + var command = new UpdateWorkflow + { + Workflow = new Workflow( + Status.Draft, + new Dictionary + { + [Status.Draft] = new WorkflowStep() + }), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), + new ValidationError("Workflow must have a published step.", "Workflow.Steps")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_step_is_not_defined() + { + var command = new UpdateWorkflow + { + Workflow = new Workflow( + Status.Draft, + new Dictionary + { + [Status.Published] = null!, + [Status.Draft] = new WorkflowStep() + }), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), + new ValidationError("Step is required.", "Workflow.Steps.Published")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_transition_is_invalid() + { + var command = new UpdateWorkflow + { + Workflow = new Workflow( + Status.Draft, + new Dictionary + { + [Status.Published] = + new WorkflowStep( + new Dictionary + { + [Status.Archived] = new WorkflowTransition() + }), + [Status.Draft] = new WorkflowStep() + }), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), + new ValidationError("Transition has an invalid target.", "Workflow.Steps.Published.Transitions.Archived")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_transition_is_not_defined() + { + var command = new UpdateWorkflow + { + Workflow = new Workflow( + Status.Draft, + new Dictionary + { + [Status.Draft] = + new WorkflowStep(), + [Status.Published] = + new WorkflowStep( + new Dictionary + { + [Status.Draft] = null! + }) + }), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), + new ValidationError("Transition is required.", "Workflow.Steps.Published.Transitions.Draft")); + } + + [Fact] + public void CanUpdate_should_not_throw_exception_if_workflow_is_valid() + { + var command = new UpdateWorkflow { Workflow = Workflow.Default, WorkflowId = workflowId }; + + GuardAppWorkflows.CanUpdate(workflows, command); + } + + [Fact] + public void CanDelete_should_throw_exception_if_workflow_not_found() + { + var command = new DeleteWorkflow { WorkflowId = Guid.NewGuid() }; + + Assert.Throws(() => GuardAppWorkflows.CanDelete(workflows, command)); + } + + [Fact] + public void CanDelete_should_not_throw_exception_if_workflow_is_found() + { + var command = new DeleteWorkflow { WorkflowId = workflowId }; + + GuardAppWorkflows.CanDelete(workflows, command); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs new file mode 100644 index 000000000..7b6468ead --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs @@ -0,0 +1,387 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsIndexTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IAppsByNameIndexGrain indexByName = A.Fake(); + private readonly IAppsByUserIndexGrain indexByUser = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly string userId = "user-1"; + private readonly AppsIndex sut; + + public AppsIndexTests() + { + A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) + .Returns(indexByName); + + A.CallTo(() => grainFactory.GetGrain(userId, null)) + .Returns(indexByUser); + + sut = new AppsIndex(grainFactory); + } + + [Fact] + public async Task Should_resolve_all_apps_from_user_permissions() + { + var expected = SetupApp(0, false); + + A.CallTo(() => indexByName.GetIdsAsync(A.That.IsSameSequenceAs(new string[] { appId.Name }))) + .Returns(new List { appId.Id }); + + var actual = await sut.GetAppsForUserAsync(userId, new PermissionSet($"squidex.apps.{appId.Name}")); + + Assert.Same(expected, actual[0]); + } + + [Fact] + public async Task Should_resolve_all_apps_from_user() + { + var expected = SetupApp(0, false); + + A.CallTo(() => indexByUser.GetIdsAsync()) + .Returns(new List { appId.Id }); + + var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty); + + Assert.Same(expected, actual[0]); + } + + [Fact] + public async Task Should_resolve_all_apps() + { + var expected = SetupApp(0, false); + + A.CallTo(() => indexByName.GetIdsAsync()) + .Returns(new List { appId.Id }); + + var actual = await sut.GetAppsAsync(); + + Assert.Same(expected, actual[0]); + } + + [Fact] + public async Task Should_resolve_app_by_name() + { + var expected = SetupApp(0, false); + + A.CallTo(() => indexByName.GetIdAsync(appId.Name)) + .Returns(appId.Id); + + var actual = await sut.GetAppByNameAsync(appId.Name); + + Assert.Same(expected, actual); + } + + [Fact] + public async Task Should_resolve_app_by_id() + { + var expected = SetupApp(0, false); + + var actual = await sut.GetAppAsync(appId.Id); + + Assert.Same(expected, actual); + } + + [Fact] + public async Task Should_return_null_if_app_archived() + { + SetupApp(0, true); + + var actual = await sut.GetAppAsync(appId.Id); + + Assert.Null(actual); + } + + [Fact] + public async Task Should_return_null_if_app_not_created() + { + SetupApp(-1, false); + + var actual = await sut.GetAppAsync(appId.Id); + + Assert.Null(actual); + } + + [Fact] + public async Task Should_add_app_to_indexes_on_create() + { + var token = RandomHash.Simple(); + + A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + .Returns(token); + + var context = + new CommandContext(Create(appId.Name), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.AddAsync(token)) + .MustHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_app_to_user_index_if_app_created_by_client() + { + var token = RandomHash.Simple(); + + A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + .Returns(token); + + var context = + new CommandContext(CreateFromClient(appId.Name), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.AddAsync(token)) + .MustHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_clear_reservation_when_app_creation_failed() + { + var token = RandomHash.Simple(); + + A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + .Returns(token); + + var context = + new CommandContext(CreateFromClient(appId.Name), commandBus); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.AddAsync(token)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(token)) + .MustHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_add_to_indexes_on_create_if_name_taken() + { + A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + .Returns(Task.FromResult(null)); + + var context = + new CommandContext(Create(appId.Name), commandBus) + .Complete(); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + + A.CallTo(() => indexByName.AddAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_add_to_indexes_on_create_if_name_invalid() + { + var context = + new CommandContext(Create("INVALID"), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.ReserveAsync(appId.Id, A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_add_app_to_index_on_contributor_assignment() + { + var context = + new CommandContext(new AssignContributor { AppId = appId.Id, ContributorId = userId }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_remove_from_user_index_on_remove_of_contributor() + { + var context = + new CommandContext(new RemoveContributor { AppId = appId.Id, ContributorId = userId }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByUser.RemoveAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_remove_app_from_indexes_on_archive() + { + var app = SetupApp(0, false); + + var context = + new CommandContext(new ArchiveApp { AppId = appId.Id }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.RemoveAsync(appId.Id)) + .MustHaveHappened(); + + A.CallTo(() => indexByUser.RemoveAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_call_when_rebuilding_for_contributors1() + { + var apps = new HashSet(); + + await sut.RebuildByContributorsAsync(userId, apps); + + A.CallTo(() => indexByUser.RebuildAsync(apps)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_call_when_rebuilding_for_contributors2() + { + var users = new HashSet { userId }; + + await sut.RebuildByContributorsAsync(appId.Id, users); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_call_when_rebuilding() + { + var apps = new Dictionary(); + + await sut.RebuildAsync(apps); + + A.CallTo(() => indexByName.RebuildAsync(apps)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_reserveration() + { + await sut.AddAsync("token"); + + A.CallTo(() => indexByName.AddAsync("token")) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_remove_reservation() + { + await sut.RemoveReservationAsync("token"); + + A.CallTo(() => indexByName.RemoveReservationAsync("token")) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_request_for_ids() + { + await sut.GetIdsAsync(); + + A.CallTo(() => indexByName.GetIdsAsync()) + .MustHaveHappened(); + } + + private IAppEntity SetupApp(long version, bool archived) + { + var appEntity = A.Fake(); + + A.CallTo(() => appEntity.Name) + .Returns(appId.Name); + A.CallTo(() => appEntity.Version) + .Returns(version); + A.CallTo(() => appEntity.IsArchived) + .Returns(archived); + A.CallTo(() => appEntity.Contributors) + .Returns(AppContributors.Empty.Assign(userId, Role.Owner)); + + var appGrain = A.Fake(); + + A.CallTo(() => appGrain.GetStateAsync()) + .Returns(J.Of(appEntity)); + + A.CallTo(() => grainFactory.GetGrain(appId.Id, null)) + .Returns(appGrain); + + return appEntity; + } + + private CreateApp Create(string name) + { + return new CreateApp { AppId = appId.Id, Name = name, Actor = ActorSubject() }; + } + + private CreateApp CreateFromClient(string name) + { + return new CreateApp { AppId = appId.Id, Name = name, Actor = ActorClient() }; + } + + private RefToken ActorSubject() + { + return new RefToken(RefTokenType.Subject, userId); + } + + private RefToken ActorClient() + { + return new RefToken(RefTokenType.Client, userId); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/AlwaysCreateClientCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/AlwaysCreateClientCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/AlwaysCreateClientCommandMiddlewareTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/AlwaysCreateClientCommandMiddlewareTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs new file mode 100644 index 000000000..ba32701fc --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs @@ -0,0 +1,149 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetChangedTriggerHandlerTests + { + private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IAssetLoader assetLoader = A.Fake(); + private readonly IRuleTriggerHandler sut; + + public AssetChangedTriggerHandlerTests() + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) + .Returns(true); + + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) + .Returns(false); + + sut = new AssetChangedTriggerHandler(scriptEngine, assetLoader); + } + + public static IEnumerable TestEvents = new[] + { + new object[] { new AssetCreated(), EnrichedAssetEventType.Created }, + new object[] { new AssetUpdated(), EnrichedAssetEventType.Updated }, + new object[] { new AssetAnnotated(), EnrichedAssetEventType.Annotated }, + new object[] { new AssetDeleted(), EnrichedAssetEventType.Deleted } + }; + + [Theory] + [MemberData(nameof(TestEvents))] + public async Task Should_enrich_events(AssetEvent @event, EnrichedAssetEventType type) + { + var envelope = Envelope.Create(@event).SetEventStreamNumber(12); + + A.CallTo(() => assetLoader.GetAsync(@event.AssetId, 12)) + .Returns(new AssetEntity()); + + var result = await sut.CreateEnrichedEventAsync(envelope) as EnrichedAssetEvent; + + Assert.Equal(type, result!.Type); + } + + [Fact] + public void Should_not_trigger_precheck_when_event_type_not_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new ContentCreated(), trigger, Guid.NewGuid()); + + Assert.False(result); + }); + } + + [Fact] + public void Should_trigger_precheck_when_event_type_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new AssetCreated(), trigger, Guid.NewGuid()); + + Assert.True(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_event_type_not_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new EnrichedContentEvent(), trigger); + + Assert.False(result); + }); + } + + [Fact] + public void Should_trigger_check_when_condition_is_empty() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_trigger_check_when_condition_matchs() + { + TestForCondition("true", trigger => + { + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_condition_does_not_matchs() + { + TestForCondition("false", trigger => + { + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); + + Assert.False(result); + }); + } + + private void TestForCondition(string condition, Action action) + { + var trigger = new AssetChangedTriggerV2 { Condition = condition }; + + action(trigger); + + if (string.IsNullOrWhiteSpace(condition)) + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) + .MustNotHaveHappened(); + } + else + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) + .MustHaveHappened(); + } + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs new file mode 100644 index 000000000..f07531700 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.IO; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure.Assets; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class FileTypeTagGeneratorTests + { + private readonly HashSet tags = new HashSet(); + private readonly FileTypeTagGenerator sut = new FileTypeTagGenerator(); + + [Fact] + public void Should_not_add_tag_if_no_file_info() + { + var command = new CreateAsset(); + + sut.GenerateTags(command, tags); + + Assert.Empty(tags); + } + + [Fact] + public void Should_add_file_type() + { + var command = new CreateAsset + { + File = new AssetFile("File.DOCX", "Mime", 100, () => new MemoryStream()) + }; + + sut.GenerateTags(command, tags); + + Assert.Contains("type/docx", tags); + } + + [Fact] + public void Should_add_blob_if_without_extension() + { + var command = new CreateAsset + { + File = new AssetFile("File", "Mime", 100, () => new MemoryStream()) + }; + + sut.GenerateTags(command, tags); + + Assert.Contains("type/blob", tags); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageTagGeneratorTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageTagGeneratorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageTagGeneratorTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageTagGeneratorTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs new file mode 100644 index 000000000..a5d5c3c77 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs @@ -0,0 +1,236 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using FakeItEasy; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using NodaTime.Text; +using Squidex.Domain.Apps.Entities.MongoDb.Assets; +using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.MongoDb.Queries; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Validation; +using Xunit; +using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; +using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; + +namespace Squidex.Domain.Apps.Entities.Assets.MongoDb +{ + public class MongoDbQueryTests + { + private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; + private static readonly IBsonSerializer Serializer = BsonSerializer.SerializerRegistry.GetSerializer(); + + static MongoDbQueryTests() + { + InstantSerializer.Register(); + } + + [Fact] + public void Should_throw_exception_for_full_text_search() + { + Assert.Throws(() => Q(new ClrQuery { FullText = "Full Text" })); + } + + [Fact] + public void Should_make_query_with_lastModified() + { + var i = F(ClrFilter.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); + var o = C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_lastModifiedBy() + { + var i = F(ClrFilter.Eq("lastModifiedBy", "Me")); + var o = C("{ 'mb' : 'Me' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_created() + { + var i = F(ClrFilter.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); + var o = C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_createdBy() + { + var i = F(ClrFilter.Eq("createdBy", "Me")); + var o = C("{ 'cb' : 'Me' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_version() + { + var i = F(ClrFilter.Eq("version", 0)); + var o = C("{ 'vs' : NumberLong(0) }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_fileVersion() + { + var i = F(ClrFilter.Eq("fileVersion", 2)); + var o = C("{ 'fv' : NumberLong(2) }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_tags() + { + var i = F(ClrFilter.Eq("tags", "tag1")); + var o = C("{ 'td' : 'tag1' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_fileName() + { + var i = F(ClrFilter.Eq("fileName", "Logo.png")); + var o = C("{ 'fn' : 'Logo.png' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_isImage() + { + var i = F(ClrFilter.Eq("isImage", true)); + var o = C("{ 'im' : true }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_mimeType() + { + var i = F(ClrFilter.Eq("mimeType", "text/json")); + var o = C("{ 'mm' : 'text/json' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_fileSize() + { + var i = F(ClrFilter.Eq("fileSize", 1024)); + var o = C("{ 'fs' : NumberLong(1024) }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_pixelHeight() + { + var i = F(ClrFilter.Eq("pixelHeight", 600)); + var o = C("{ 'ph' : 600 }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_pixelWidth() + { + var i = F(ClrFilter.Eq("pixelWidth", 800)); + var o = C("{ 'pw' : 800 }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_orderby_with_single_field() + { + var i = S(SortBuilder.Descending("lastModified")); + var o = C("{ 'mt' : -1 }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_orderby_with_multiple_fields() + { + var i = S(SortBuilder.Ascending("lastModified"), SortBuilder.Descending("lastModifiedBy")); + var o = C("{ 'mt' : 1, 'mb' : -1 }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_take_statement() + { + var query = new ClrQuery { Take = 3 }; + var cursor = A.Fake>(); + + cursor.AssetTake(query.AdjustToModel()); + + A.CallTo(() => cursor.Limit(3)) + .MustHaveHappened(); + } + + [Fact] + public void Should_make_skip_statement() + { + var query = new ClrQuery { Skip = 3 }; + var cursor = A.Fake>(); + + cursor.AssetSkip(query.AdjustToModel()); + + A.CallTo(() => cursor.Skip(3)) + .MustHaveHappened(); + } + + private static string C(string value) + { + return value.Replace('\'', '"'); + } + + private static string F(FilterNode filter) + { + return Q(new ClrQuery { Filter = filter }); + } + + private static string S(params SortNode[] sorts) + { + var cursor = A.Fake>(); + + var i = string.Empty; + + A.CallTo(() => cursor.Sort(A>.Ignored)) + .Invokes((SortDefinition sortDefinition) => + { + i = sortDefinition.Render(Serializer, Registry).ToString(); + }); + + cursor.AssetSort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel()); + + return i; + } + + private static string Q(ClrQuery query) + { + var rendered = + query.AdjustToModel().BuildFilter(false).Filter! + .Render(Serializer, Registry).ToString(); + + return rendered; + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs new file mode 100644 index 000000000..b04d6c395 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public class AssetLoaderTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IAssetGrain grain = A.Fake(); + private readonly Guid id = Guid.NewGuid(); + private readonly AssetLoader sut; + + public AssetLoaderTests() + { + A.CallTo(() => grainFactory.GetGrain(id, null)) + .Returns(grain); + + sut = new AssetLoader(grainFactory); + } + + [Fact] + public async Task Should_throw_exception_if_no_state_returned() + { + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(null!)); + + await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); + } + + [Fact] + public async Task Should_throw_exception_if_state_has_other_version() + { + var content = new AssetEntity { Version = 5 }; + + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(content)); + + await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); + } + + [Fact] + public async Task Should_return_content_from_state() + { + var content = new AssetEntity { Version = 10 }; + + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(content)); + + var result = await sut.GetAsync(id, 10); + + Assert.Same(content, result); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs new file mode 100644 index 000000000..e0900ded3 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure.Queries; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public class FilterTagTransformerTests + { + private readonly ITagService tagService = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + + [Fact] + public void Should_normalize_tags() + { + A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A>.That.Contains("name1"))) + .Returns(new Dictionary { ["name1"] = "id1" }); + + var source = ClrFilter.Eq("tags", "name1"); + + var result = FilterTagTransformer.Transform(source, appId, tagService); + + Assert.Equal("tags == 'id1'", result!.ToString()); + } + + [Fact] + public void Should_not_fail_when_tags_not_found() + { + A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A>.That.Contains("name1"))) + .Returns(new Dictionary()); + + var source = ClrFilter.Eq("tags", "name1"); + + var result = FilterTagTransformer.Transform(source, appId, tagService); + + Assert.Equal("tags == 'name1'", result!.ToString()); + } + + [Fact] + public void Should_not_normalize_other_field() + { + var source = ClrFilter.Eq("other", "value"); + + var result = FilterTagTransformer.Transform(source, appId, tagService); + + Assert.Equal("other == 'value'", result!.ToString()); + + A.CallTo(() => tagService.GetTagIdsAsync(appId, A.Ignored, A>.Ignored)) + .MustNotHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs new file mode 100644 index 000000000..39d41bbe0 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -0,0 +1,235 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +#pragma warning disable SA1401 // Fields must be private +#pragma warning disable RECS0070 + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentChangedTriggerHandlerTests + { + private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IContentLoader contentLoader = A.Fake(); + private readonly IRuleTriggerHandler sut; + private readonly Guid ruleId = Guid.NewGuid(); + private static readonly NamedId SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1"); + private static readonly NamedId SchemaNonMatch = NamedId.Of(Guid.NewGuid(), "my-schema2"); + + public ContentChangedTriggerHandlerTests() + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) + .Returns(true); + + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) + .Returns(false); + + sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader); + } + + public static IEnumerable TestEvents = new[] + { + new object[] { new ContentCreated(), EnrichedContentEventType.Created }, + new object[] { new ContentUpdated(), EnrichedContentEventType.Updated }, + new object[] { new ContentDeleted(), EnrichedContentEventType.Deleted }, + new object[] { new ContentStatusChanged { Change = StatusChange.Change }, EnrichedContentEventType.StatusChanged }, + new object[] { new ContentStatusChanged { Change = StatusChange.Published }, EnrichedContentEventType.Published }, + new object[] { new ContentStatusChanged { Change = StatusChange.Unpublished }, EnrichedContentEventType.Unpublished } + }; + + [Theory] + [MemberData(nameof(TestEvents))] + public async Task Should_enrich_events(ContentEvent @event, EnrichedContentEventType type) + { + var envelope = Envelope.Create(@event).SetEventStreamNumber(12); + + A.CallTo(() => contentLoader.GetAsync(@event.ContentId, 12)) + .Returns(new ContentEntity { SchemaId = SchemaMatch }); + + var result = await sut.CreateEnrichedEventAsync(envelope) as EnrichedContentEvent; + + Assert.Equal(type, result!.Type); + } + + [Fact] + public void Should_not_trigger_precheck_when_event_type_not_correct() + { + TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => + { + var result = sut.Trigger(new AssetCreated(), trigger, ruleId); + + Assert.False(result); + }); + } + + [Fact] + public void Should_not_trigger_precheck_when_trigger_contains_no_schemas() + { + TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => + { + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); + + Assert.False(result); + }); + } + + [Fact] + public void Should_trigger_precheck_when_handling_all_events() + { + TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => + { + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); + + Assert.True(result); + }); + } + + [Fact] + public void Should_trigger_precheck_when_condition_is_empty() + { + TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => + { + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); + + Assert.True(result); + }); + } + + [Fact] + public void Should_not_trigger_precheck_when_schema_id_does_not_match() + { + TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => + { + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); + + Assert.False(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_event_type_not_correct() + { + TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => + { + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); + + Assert.False(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_trigger_contains_no_schemas() + { + TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => + { + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + + Assert.False(result); + }); + } + + [Fact] + public void Should_trigger_check_when_handling_all_events() + { + TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => + { + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_trigger_check_when_condition_is_empty() + { + TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => + { + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_trigger_check_when_condition_matchs() + { + TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "true", action: trigger => + { + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_schema_id_does_not_match() + { + TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => + { + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + + Assert.False(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_condition_does_not_matchs() + { + TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "false", action: trigger => + { + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + + Assert.False(result); + }); + } + + private void TestForTrigger(bool handleAll, NamedId? schemaId, string? condition, Action action) + { + var trigger = new ContentChangedTriggerV2 { HandleAll = handleAll }; + + if (schemaId != null) + { + trigger.Schemas = new ReadOnlyCollection(new List + { + new ContentChangedTriggerSchemaV2 + { + SchemaId = schemaId.Id, Condition = condition + } + }); + } + + action(trigger); + + if (string.IsNullOrWhiteSpace(condition)) + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + else + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) + .MustHaveHappened(); + } + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs new file mode 100644 index 000000000..77f68ed0b --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs @@ -0,0 +1,592 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using NodaTime; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentGrainTests : HandlerTestBase + { + private readonly Guid contentId = Guid.NewGuid(); + private readonly IActivationLimit limit = A.Fake(); + private readonly IAppEntity app; + private readonly IAppProvider appProvider = A.Fake(); + private readonly IContentRepository contentRepository = A.Dummy(); + private readonly IContentWorkflow contentWorkflow = A.Fake(x => x.Wrapping(new DefaultContentWorkflow())); + private readonly ISchemaEntity schema; + private readonly IScriptEngine scriptEngine = A.Fake(); + + private readonly NamedContentData invalidData = + new NamedContentData() + .AddField("my-field1", + new ContentFieldData() + .AddValue("iv", null)) + .AddField("my-field2", + new ContentFieldData() + .AddValue("iv", 1)); + private readonly NamedContentData data = + new NamedContentData() + .AddField("my-field1", + new ContentFieldData() + .AddValue("iv", 1)); + private readonly NamedContentData patch = + new NamedContentData() + .AddField("my-field2", + new ContentFieldData() + .AddValue("iv", 2)); + private readonly NamedContentData otherData = + new NamedContentData() + .AddField("my-field1", + new ContentFieldData() + .AddValue("iv", 2)) + .AddField("my-field2", + new ContentFieldData() + .AddValue("iv", 2)); + private readonly NamedContentData patched; + private readonly ContentGrain sut; + + protected override Guid Id + { + get { return contentId; } + } + + public ContentGrainTests() + { + app = Mocks.App(AppNamedId, Language.DE); + + var scripts = new SchemaScripts + { + Change = "", + Create = "", + Delete = "", + Update = "" + }; + + var schemaDef = + new Schema("my-schema") + .AddNumber(1, "my-field1", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = true }) + .AddNumber(2, "my-field2", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = false }) + .ConfigureScripts(scripts); + + schema = Mocks.Schema(AppNamedId, SchemaNamedId, schemaDef); + + A.CallTo(() => appProvider.GetAppAsync(AppName)) + .Returns(app); + + A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)) + .Returns((app, schema)); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .ReturnsLazily(x => x.GetArgument(0).Data!); + + patched = patch.MergeInto(data); + + sut = new ContentGrain(Store, A.Dummy(), appProvider, A.Dummy(), scriptEngine, contentWorkflow, contentRepository, limit); + sut.ActivateAsync(Id).Wait(); + } + + [Fact] + public void Should_set_limit() + { + A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5))) + .MustHaveHappened(); + } + + [Fact] + public async Task Command_should_throw_exception_if_content_is_deleted() + { + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); + + await Assert.ThrowsAsync(ExecuteUpdateAsync); + } + + [Fact] + public async Task Create_should_create_events_and_update_state() + { + var command = new CreateContent { Data = data }; + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Draft, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(data, null, Status.Draft), "")) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Create_should_also_publish() + { + var command = new CreateContent { Data = data, Publish = true }; + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Published, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }), + CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(data, null, Status.Draft), "")) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Published), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task Create_should_throw_when_invalid_data_is_passed() + { + var command = new CreateContent { Data = invalidData }; + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(CreateContentCommand(command))); + } + + [Fact] + public async Task Update_should_create_events_and_update_state() + { + var command = new UpdateContent { Data = otherData }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(otherData, data, Status.Draft), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task Update_should_create_proposal_events_and_update_state() + { + var command = new UpdateContent { Data = otherData, AsDraft = true }; + + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.True(sut.Snapshot.IsPending); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdateProposed { Data = otherData }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(otherData, data, Status.Published), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task Update_should_not_create_event_for_same_data() + { + var command = new UpdateContent { Data = data }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Single(LastEvents); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Update_should_throw_when_invalid_data_is_passed() + { + var command = new UpdateContent { Data = invalidData }; + + await ExecuteCreateAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(CreateContentCommand(command))); + } + + [Fact] + public async Task Patch_should_create_events_and_update_state() + { + var command = new PatchContent { Data = patch }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = patched }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(patched, data, Status.Draft), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task Patch_should_create_proposal_events_and_update_state() + { + var command = new PatchContent { Data = patch, AsDraft = true }; + + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.True(sut.Snapshot.IsPending); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdateProposed { Data = patched }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(patched, data, Status.Published), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task Patch_should_not_create_event_for_same_data() + { + var command = new PatchContent { Data = data }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Single(LastEvents); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_create_events_and_update_state() + { + var command = new ChangeContentStatus { Status = Status.Published }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Published, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Published, Status = Status.Published }) + ); + + A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Published, Status.Draft), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_create_events_and_update_state_when_archived() + { + var command = new ChangeContentStatus { Status = Status.Archived }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Archived, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) + ); + + A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Archived, Status.Draft), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_create_events_and_update_state_when_unpublished() + { + var command = new ChangeContentStatus { Status = Status.Draft }; + + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Draft, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Unpublished, Status = Status.Draft }) + ); + + A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft, Status.Published), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_create_events_and_update_state_when_restored() + { + var command = new ChangeContentStatus { Status = Status.Draft }; + + await ExecuteCreateAsync(); + await ExecuteArchiveAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Draft, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Status = Status.Draft }) + ); + + A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft, Status.Archived), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_create_proposal_events_and_update_state() + { + var command = new ChangeContentStatus { Status = Status.Published }; + + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + await ExecuteProposeUpdateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.False(sut.Snapshot.IsPending); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentChangesPublished()) + ); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_refresh_properties_and_create_scheduled_events_when_command_has_due_time() + { + var dueTime = Instant.MaxValue; + + var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTime }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Draft, sut.Snapshot.Status); + Assert.Equal(Status.Published, sut.Snapshot.ScheduleJob!.Status); + Assert.Equal(dueTime, sut.Snapshot.ScheduleJob.DueTime); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime }) + ); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_refresh_properties_and_revert_scheduling_when_invoked_by_scheduler() + { + await ExecuteCreateAsync(); + await ExecuteChangeStatusAsync(Status.Published, Instant.MaxValue); + + var command = new ChangeContentStatus { Status = Status.Published, JobId = sut.Snapshot.ScheduleJob!.Id }; + + A.CallTo(() => contentWorkflow.CanMoveToAsync(A.Ignored, Status.Published, User)) + .Returns(false); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Null(sut.Snapshot.ScheduleJob); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentSchedulingCancelled()) + ); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Delete_should_update_properties_and_create_events() + { + var command = new DeleteContent(); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(1)); + + Assert.True(sut.Snapshot.IsDeleted); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentDeleted()) + ); + + A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task DiscardChanges_should_update_properties_and_create_events() + { + var command = new DiscardChanges(); + + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + await ExecuteProposeUpdateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(3)); + + Assert.False(sut.Snapshot.IsPending); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentChangesDiscarded()) + ); + } + + private Task ExecuteCreateAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new CreateContent { Data = data })); + } + + private Task ExecuteUpdateAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new UpdateContent { Data = otherData })); + } + + private Task ExecuteProposeUpdateAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new UpdateContent { Data = otherData, AsDraft = true })); + } + + private Task ExecuteChangeStatusAsync(Status status, Instant? dueTime = null) + { + return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = status, DueTime = dueTime })); + } + + private Task ExecuteDeleteAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new DeleteContent())); + } + + private Task ExecuteArchiveAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Archived })); + } + + private Task ExecutePublishAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); + } + + private ScriptContext ScriptContext(NamedContentData? newData, NamedContentData? oldData, Status newStatus) + { + return A.That.Matches(x => M(x, newData, oldData, newStatus, default)); + } + + private ScriptContext ScriptContext(NamedContentData? newData, NamedContentData? oldData, Status newStatus, Status oldStatus) + { + return A.That.Matches(x => M(x, newData, oldData, newStatus, oldStatus)); + } + + private bool M(ScriptContext x, NamedContentData? newData, NamedContentData? oldData, Status newStatus, Status oldStatus) + { + return + Equals(x.Data, newData) && + Equals(x.DataOld, oldData) && + Equals(x.Status, newStatus) && + Equals(x.StatusOld, oldStatus) && + x.ContentId == contentId && x.User == User; + } + + protected T CreateContentEvent(T @event) where T : ContentEvent + { + @event.ContentId = contentId; + + return CreateEvent(@event); + } + + protected T CreateContentCommand(T command) where T : ContentCommand + { + command.ContentId = contentId; + + return CreateCommand(command); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs new file mode 100644 index 000000000..093d7da92 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs @@ -0,0 +1,139 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Contents; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class DefaultContentWorkflowTests + { + private readonly DefaultContentWorkflow sut = new DefaultContentWorkflow(); + + [Fact] + public async Task Should_always_allow_publish_on_create() + { + var result = await sut.CanPublishOnCreateAsync(null!, null!, null!); + + Assert.True(result); + } + + [Fact] + public async Task Should_draft_as_initial_status() + { + var expected = new StatusInfo(Status.Draft, StatusColors.Draft); + + var result = await sut.GetInitialStatusAsync(null!); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_check_is_valid_next() + { + var content = new ContentEntity { Status = Status.Published }; + + var result = await sut.CanMoveToAsync(content, Status.Draft, null!); + + Assert.True(result); + } + + [Fact] + public async Task Should_be_able_to_update_published() + { + var content = new ContentEntity { Status = Status.Published }; + + var result = await sut.CanUpdateAsync(content); + + Assert.True(result); + } + + [Fact] + public async Task Should_be_able_to_update_draft() + { + var content = new ContentEntity { Status = Status.Published }; + + var result = await sut.CanUpdateAsync(content); + + Assert.True(result); + } + + [Fact] + public async Task Should_not_be_able_to_update_archived() + { + var content = new ContentEntity { Status = Status.Archived }; + + var result = await sut.CanUpdateAsync(content); + + Assert.False(result); + } + + [Fact] + public async Task Should_get_next_statuses_for_draft() + { + var content = new ContentEntity { Status = Status.Draft }; + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Published, StatusColors.Published) + }; + + var result = await sut.GetNextsAsync(content, null!); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_get_next_statuses_for_archived() + { + var content = new ContentEntity { Status = Status.Archived }; + + var expected = new[] + { + new StatusInfo(Status.Draft, StatusColors.Draft) + }; + + var result = await sut.GetNextsAsync(content, null!); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_get_next_statuses_for_published() + { + var content = new ContentEntity { Status = Status.Published }; + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft) + }; + + var result = await sut.GetNextsAsync(content, null!); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_return_all_statuses() + { + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft), + new StatusInfo(Status.Published, StatusColors.Published) + }; + + var result = await sut.GetAllAsync(null!); + + result.Should().BeEquivalentTo(expected); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs new file mode 100644 index 000000000..73991874e --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +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.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class DefaultWorkflowsValidatorTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly DefaultWorkflowsValidator sut; + + public DefaultWorkflowsValidatorTests() + { + var schema = Mocks.Schema(appId, schemaId, new Schema(schemaId.Name)); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) + .Returns(Task.FromResult(null)); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + .Returns(schema); + + sut = new DefaultWorkflowsValidator(appProvider); + } + + [Fact] + public async Task Should_generate_error_if_multiple_workflows_cover_all_schemas() + { + var workflows = Workflows.Empty + .Add(Guid.NewGuid(), "workflow1") + .Add(Guid.NewGuid(), "workflow2"); + + var errors = await sut.ValidateAsync(appId.Id, workflows); + + Assert.Equal(errors, new[] { "Multiple workflows cover all schemas." }); + } + + [Fact] + public async Task Should_generate_error_if_multiple_workflows_cover_specific_schema() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var workflows = Workflows.Empty + .Add(id1, "workflow1") + .Add(id2, "workflow2") + .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })) + .Update(id2, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })); + + var errors = await sut.ValidateAsync(appId.Id, workflows); + + Assert.Equal(errors, new[] { "The schema `my-schema` is covered by multiple workflows." }); + } + + [Fact] + public async Task Should_not_generate_error_if_schema_deleted() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var oldSchemaId = Guid.NewGuid(); + + var workflows = Workflows.Empty + .Add(id1, "workflow1") + .Add(id2, "workflow2") + .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { oldSchemaId })) + .Update(id2, new Workflow(default, Workflow.EmptySteps, new List { oldSchemaId })); + + var errors = await sut.ValidateAsync(appId.Id, workflows); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_generate_errors_for_no_overlaps() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var workflows = Workflows.Empty + .Add(id1, "workflow1") + .Add(id2, "workflow2") + .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })); + + var errors = await sut.ValidateAsync(appId.Id, workflows); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_generate_errors_for_empty_workflows() + { + var errors = await sut.ValidateAsync(appId.Id, Workflows.Empty); + + Assert.Empty(errors); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs new file mode 100644 index 000000000..25b4389fc --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs @@ -0,0 +1,352 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class DynamicContentWorkflowTests + { + private readonly IAppEntity app; + private readonly IAppProvider appProvider = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly NamedId simpleSchemaId = NamedId.Of(Guid.NewGuid(), "my-simple-schema"); + private readonly DynamicContentWorkflow sut; + + private readonly Workflow workflow = new Workflow( + Status.Draft, + new Dictionary + { + [Status.Archived] = + new WorkflowStep( + new Dictionary + { + [Status.Draft] = new WorkflowTransition() + }, + StatusColors.Archived, true), + [Status.Draft] = + new WorkflowStep( + new Dictionary + { + [Status.Archived] = new WorkflowTransition(), + [Status.Published] = new WorkflowTransition("data.field.iv === 2", ReadOnlyCollection.Create("Owner", "Editor")) + }, + StatusColors.Draft), + [Status.Published] = + new WorkflowStep( + new Dictionary + { + [Status.Archived] = new WorkflowTransition(), + [Status.Draft] = new WorkflowTransition() + }, + StatusColors.Published) + }); + + public DynamicContentWorkflowTests() + { + app = Mocks.App(appId); + + var simpleWorkflow = new Workflow( + Status.Draft, + new Dictionary + { + [Status.Draft] = + new WorkflowStep( + new Dictionary + { + [Status.Published] = new WorkflowTransition() + }, + StatusColors.Draft), + [Status.Published] = + new WorkflowStep( + new Dictionary + { + [Status.Draft] = new WorkflowTransition() + }, + StatusColors.Published) + }, + new List { simpleSchemaId.Id }); + + var workflows = Workflows.Empty.Set(workflow).Set(Guid.NewGuid(), simpleWorkflow); + + A.CallTo(() => appProvider.GetAppAsync(appId.Id)) + .Returns(app); + + A.CallTo(() => app.Workflows) + .Returns(workflows); + + sut = new DynamicContentWorkflow(new JintScriptEngine(), appProvider); + } + + [Fact] + public async Task Should_draft_as_initial_status() + { + var expected = new StatusInfo(Status.Draft, StatusColors.Draft); + + var result = await sut.GetInitialStatusAsync(Mocks.Schema(appId, schemaId)); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_allow_publish_on_create() + { + var content = CreateContent(Status.Draft, 2); + + var result = await sut.CanPublishOnCreateAsync(Mocks.Schema(appId, schemaId), content.DataDraft, Mocks.FrontendUser("Editor")); + + Assert.True(result); + } + + [Fact] + public async Task Should_not_allow_publish_on_create_if_data_is_invalid() + { + var content = CreateContent(Status.Draft, 4); + + var result = await sut.CanPublishOnCreateAsync(Mocks.Schema(appId, schemaId), content.DataDraft, Mocks.FrontendUser("Editor")); + + Assert.False(result); + } + + [Fact] + public async Task Should_not_allow_publish_on_create_if_role_not_allowed() + { + var content = CreateContent(Status.Draft, 2); + + var result = await sut.CanPublishOnCreateAsync(Mocks.Schema(appId, schemaId), content.DataDraft, Mocks.FrontendUser("Developer")); + + Assert.False(result); + } + + [Fact] + public async Task Should_check_is_valid_next() + { + var content = CreateContent(Status.Draft, 2); + + var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Editor")); + + Assert.True(result); + } + + [Fact] + public async Task Should_not_allow_transition_if_role_is_not_allowed() + { + var content = CreateContent(Status.Draft, 2); + + var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Developer")); + + Assert.False(result); + } + + [Fact] + public async Task Should_allow_transition_if_role_is_allowed() + { + var content = CreateContent(Status.Draft, 2); + + var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Editor")); + + Assert.True(result); + } + + [Fact] + public async Task Should_not_allow_transition_if_data_not_valid() + { + var content = CreateContent(Status.Draft, 4); + + var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Editor")); + + Assert.False(result); + } + + [Fact] + public async Task Should_be_able_to_update_published() + { + var content = CreateContent(Status.Published, 2); + + var result = await sut.CanUpdateAsync(content); + + Assert.True(result); + } + + [Fact] + public async Task Should_be_able_to_update_draft() + { + var content = CreateContent(Status.Published, 2); + + var result = await sut.CanUpdateAsync(content); + + Assert.True(result); + } + + [Fact] + public async Task Should_not_be_able_to_update_archived() + { + var content = CreateContent(Status.Archived, 2); + + var result = await sut.CanUpdateAsync(content); + + Assert.False(result); + } + + [Fact] + public async Task Should_get_next_statuses_for_draft() + { + var content = CreateContent(Status.Draft, 2); + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived) + }; + + var result = await sut.GetNextsAsync(content, Mocks.FrontendUser("Developer")); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_limit_next_statuses_if_expression_does_not_evauate_to_true() + { + var content = CreateContent(Status.Draft, 4); + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived) + }; + + var result = await sut.GetNextsAsync(content, Mocks.FrontendUser("Editor")); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_limit_next_statuses_if_role_is_not_allowed() + { + var content = CreateContent(Status.Draft, 2); + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Published, StatusColors.Published) + }; + + var result = await sut.GetNextsAsync(content, Mocks.FrontendUser("Editor")); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_get_next_statuses_for_archived() + { + var content = CreateContent(Status.Archived, 2); + + var expected = new[] + { + new StatusInfo(Status.Draft, StatusColors.Draft) + }; + + var result = await sut.GetNextsAsync(content, null!); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_get_next_statuses_for_published() + { + var content = CreateContent(Status.Published, 2); + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft) + }; + + var result = await sut.GetNextsAsync(content, null!); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_return_all_statuses() + { + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft), + new StatusInfo(Status.Published, StatusColors.Published) + }; + + var result = await sut.GetAllAsync(Mocks.Schema(appId, schemaId)); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_return_all_statuses_for_simple_schema_workflow() + { + var expected = new[] + { + new StatusInfo(Status.Draft, StatusColors.Draft), + new StatusInfo(Status.Published, StatusColors.Published) + }; + + var result = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_return_all_statuses_for_default_workflow_when_no_workflow_configured() + { + A.CallTo(() => app.Workflows).Returns(Workflows.Empty); + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft), + new StatusInfo(Status.Published, StatusColors.Published) + }; + + var result = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); + + result.Should().BeEquivalentTo(expected); + } + + private IContentEntity CreateContent(Status status, int value, bool simple = false) + { + var content = new ContentEntity { AppId = appId, Status = status }; + + if (simple) + { + content.SchemaId = simpleSchemaId; + } + else + { + content.SchemaId = schemaId; + } + + content.DataDraft = + new NamedContentData() + .AddField("field", + new ContentFieldData() + .AddValue("iv", value)); + + return content; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs new file mode 100644 index 000000000..fb9005e86 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -0,0 +1,1270 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public class GraphQLQueriesTests : GraphQLTestBase + { + [Fact] + public async Task Should_introspect() + { + const string query = @" + query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + args { + ...InputValue + } + onOperation + onFragment + onField + } + } + } + + fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + + fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue + } + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + }"; + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, OperationName = "IntrospectionQuery" }); + + var json = serializer.Serialize(result.Response, true); + + Assert.NotEmpty(json); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task Should_return_empty_object_for_empty_query(string query) + { + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_multiple_assets_when_querying_assets() + { + const string query = @" + query { + queryAssets(filter: ""my-query"", top: 30, skip: 5) { + id + version + created + createdBy + lastModified + lastModifiedBy + url + thumbnailUrl + sourceUrl + mimeType + fileName + fileHash + fileSize + fileVersion + isImage + pixelWidth + pixelHeight + tags + slug + } + }"; + + var asset = CreateAsset(Guid.NewGuid()); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query"))) + .Returns(ResultList.CreateFrom(0, asset)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryAssets = new dynamic[] + { + new + { + id = asset.Id, + version = 1, + created = asset.Created, + createdBy = "subject:user1", + lastModified = asset.LastModified, + lastModifiedBy = "subject:user2", + url = $"assets/{asset.Id}", + thumbnailUrl = $"assets/{asset.Id}?width=100", + sourceUrl = $"assets/source/{asset.Id}", + mimeType = "image/png", + fileName = "MyFile.png", + fileHash = "ABC123", + fileSize = 1024, + fileVersion = 123, + isImage = true, + pixelWidth = 800, + pixelHeight = 600, + tags = new[] { "tag1", "tag2" }, + slug = "myfile.png" + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_multiple_assets_with_total_when_querying_assets_with_total() + { + const string query = @" + query { + queryAssetsWithTotal(filter: ""my-query"", top: 30, skip: 5) { + total + items { + id + version + created + createdBy + lastModified + lastModifiedBy + url + thumbnailUrl + sourceUrl + mimeType + fileName + fileHash + fileSize + fileVersion + isImage + pixelWidth + pixelHeight + tags + slug + } + } + }"; + + var asset = CreateAsset(Guid.NewGuid()); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query"))) + .Returns(ResultList.CreateFrom(10, asset)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryAssetsWithTotal = new + { + total = 10, + items = new dynamic[] + { + new + { + id = asset.Id, + version = 1, + created = asset.Created, + createdBy = "subject:user1", + lastModified = asset.LastModified, + lastModifiedBy = "subject:user2", + url = $"assets/{asset.Id}", + thumbnailUrl = $"assets/{asset.Id}?width=100", + sourceUrl = $"assets/source/{asset.Id}", + mimeType = "image/png", + fileName = "MyFile.png", + fileHash = "ABC123", + fileSize = 1024, + fileVersion = 123, + isImage = true, + pixelWidth = 800, + pixelHeight = 600, + tags = new[] { "tag1", "tag2" }, + slug = "myfile.png" + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_null_single_asset() + { + var assetId = Guid.NewGuid(); + + var query = @" + query { + findAsset(id: """") { + id + } + }".Replace("", assetId.ToString()); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId))) + .Returns(ResultList.CreateFrom(1)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findAsset = (object?)null + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_single_asset_when_finding_asset() + { + var assetId = Guid.NewGuid(); + var asset = CreateAsset(assetId); + + var query = @" + query { + findAsset(id: """") { + id + version + created + createdBy + lastModified + lastModifiedBy + url + thumbnailUrl + sourceUrl + mimeType + fileName + fileHash + fileSize + fileVersion + isImage + pixelWidth + pixelHeight + tags + slug + } + }".Replace("", assetId.ToString()); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId))) + .Returns(ResultList.CreateFrom(1, asset)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findAsset = new + { + id = asset.Id, + version = 1, + created = asset.Created, + createdBy = "subject:user1", + lastModified = asset.LastModified, + lastModifiedBy = "subject:user2", + url = $"assets/{asset.Id}", + thumbnailUrl = $"assets/{asset.Id}?width=100", + sourceUrl = $"assets/source/{asset.Id}", + mimeType = "image/png", + fileName = "MyFile.png", + fileHash = "ABC123", + fileSize = 1024, + fileVersion = 123, + isImage = true, + pixelWidth = 800, + pixelHeight = 600, + tags = new[] { "tag1", "tag2" }, + slug = "myfile.png" + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_multiple_contents_when_querying_contents() + { + const string query = @" + query { + queryMySchemaContents(top: 30, skip: 5) { + id + version + created + createdBy + lastModified + lastModifiedBy + status + statusColor + url + data { + myString { + de + } + myNumber { + iv + } + myBoolean { + iv + } + myDatetime { + iv + } + myJson { + iv + } + myGeolocation { + iv + } + myTags { + iv + } + myLocalized { + de_DE + } + myArray { + iv { + nestedNumber + nestedBoolean + } + } + } + } + }"; + + var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) + .Returns(ResultList.CreateFrom(0, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryMySchemaContents = new dynamic[] + { + new + { + id = content.Id, + version = 1, + created = content.Created, + createdBy = "subject:user1", + lastModified = content.LastModified, + lastModifiedBy = "subject:user2", + status = "DRAFT", + statusColor = "red", + url = $"contents/my-schema/{content.Id}", + data = new + { + myString = new + { + de = "value" + }, + myNumber = new + { + iv = 1 + }, + myBoolean = new + { + iv = true + }, + myDatetime = new + { + iv = content.LastModified + }, + myJson = new + { + iv = new + { + value = 1 + } + }, + myGeolocation = new + { + iv = new + { + latitude = 10, + longitude = 20 + } + }, + myTags = new + { + iv = new[] + { + "tag1", + "tag2" + } + }, + myLocalized = new + { + de_DE = "de-DE" + }, + myArray = new + { + iv = new[] + { + new + { + nestedNumber = 10, + nestedBoolean = true + }, + new + { + nestedNumber = 20, + nestedBoolean = false + } + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_multiple_contents_with_total_when_querying_contents_with_total() + { + const string query = @" + query { + queryMySchemaContentsWithTotal(top: 30, skip: 5) { + total + items { + id + version + created + createdBy + lastModified + lastModifiedBy + status + statusColor + url + data { + myString { + de + } + myNumber { + iv + } + myBoolean { + iv + } + myDatetime { + iv + } + myJson { + iv + } + myGeolocation { + iv + } + myTags { + iv + } + myLocalized { + de_DE + } + } + } + } + }"; + + var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) + .Returns(ResultList.CreateFrom(10, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryMySchemaContentsWithTotal = new + { + total = 10, + items = new dynamic[] + { + new + { + id = content.Id, + version = 1, + created = content.Created, + createdBy = "subject:user1", + lastModified = content.LastModified, + lastModifiedBy = "subject:user2", + status = "DRAFT", + statusColor = "red", + url = $"contents/my-schema/{content.Id}", + data = new + { + myString = new + { + de = "value" + }, + myNumber = new + { + iv = 1 + }, + myBoolean = new + { + iv = true + }, + myDatetime = new + { + iv = content.LastModified + }, + myJson = new + { + iv = new + { + value = 1 + } + }, + myGeolocation = new + { + iv = new + { + latitude = 10, + longitude = 20 + } + }, + myTags = new + { + iv = new[] + { + "tag1", + "tag2" + } + }, + myLocalized = new + { + de_DE = "de-DE" + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_single_content_with_duplicate_names() + { + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, Guid.Empty); + + var query = @" + query { + findMySchemaContent(id: """") { + data { + myNumber { + iv + } + myNumber2 { + iv + } + myArray { + iv { + nestedNumber + nestedNumber2 + } + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + data = new + { + myNumber = new + { + iv = 1 + }, + myNumber2 = new + { + iv = 2 + }, + myArray = new + { + iv = new[] + { + new + { + nestedNumber = 10, + nestedNumber2 = 11 + }, + new + { + nestedNumber = 20, + nestedNumber2 = 21 + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_null_single_content() + { + var contentId = Guid.NewGuid(); + + var query = @" + query { + findMySchemaContent(id: """") { + id + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = (object?)null + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_single_content_when_finding_content() + { + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, Guid.Empty); + + var query = @" + query { + findMySchemaContent(id: """") { + id + version + created + createdBy + lastModified + lastModifiedBy + status + statusColor + url + data { + myString { + de + } + myNumber { + iv + } + myBoolean { + iv + } + myDatetime { + iv + } + myJson { + iv + } + myGeolocation { + iv + } + myTags { + iv + } + myLocalized { + de_DE + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = content.Id, + version = 1, + created = content.Created, + createdBy = "subject:user1", + lastModified = content.LastModified, + lastModifiedBy = "subject:user2", + status = "DRAFT", + statusColor = "red", + url = $"contents/my-schema/{content.Id}", + data = new + { + myString = new + { + de = "value" + }, + myNumber = new + { + iv = 1 + }, + myBoolean = new + { + iv = true + }, + myDatetime = new + { + iv = content.LastModified + }, + myJson = new + { + iv = new + { + value = 1 + } + }, + myGeolocation = new + { + iv = new + { + latitude = 10, + longitude = 20 + } + }, + myTags = new + { + iv = new[] + { + "tag1", + "tag2" + } + }, + myLocalized = new + { + de_DE = "de-DE" + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_also_fetch_referenced_contents_when_field_is_included_in_query() + { + var contentRefId = Guid.NewGuid(); + var contentRef = CreateRefContent(schemaRefId1, contentRefId, "ref1-field", "ref1"); + + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, contentRefId, Guid.Empty); + + var query = @" + query { + findMySchemaContent(id: """") { + id + data { + myReferences { + iv { + id + data { + ref1Field { + iv + } + } + } + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>.Ignored)) + .Returns(ResultList.CreateFrom(0, contentRef)); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = content.Id, + data = new + { + myReferences = new + { + iv = new[] + { + new + { + id = contentRefId, + data = new + { + ref1Field = new + { + iv = "ref1" + } + } + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_also_fetch_union_contents_when_field_is_included_in_query() + { + var contentRefId = Guid.NewGuid(); + var contentRef = CreateRefContent(schemaRefId1, contentRefId, "ref1-field", "ref1"); + + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, contentRefId, Guid.Empty); + + var query = @" + query { + findMySchemaContent(id: """") { + id + data { + myUnion { + iv { + ... on Content { + id + } + ... on MyRefSchema1 { + data { + ref1Field { + iv + } + } + } + __typename + } + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>.Ignored)) + .Returns(ResultList.CreateFrom(0, contentRef)); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = content.Id, + data = new + { + myUnion = new + { + iv = new[] + { + new + { + id = contentRefId, + data = new + { + ref1Field = new + { + iv = "ref1" + } + }, + __typename = "MyRefSchema1" + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_also_fetch_referenced_assets_when_field_is_included_in_query() + { + var assetRefId = Guid.NewGuid(); + var assetRef = CreateAsset(assetRefId); + + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, assetRefId); + + var query = @" + query { + findMySchemaContent(id: """") { + id + data { + myAssets { + iv { + id + } + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.Ignored)) + .Returns(ResultList.CreateFrom(0, assetRef)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = content.Id, + data = new + { + myAssets = new + { + iv = new[] + { + new + { + id = assetRefId + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_make_multiple_queries() + { + var assetId1 = Guid.NewGuid(); + var assetId2 = Guid.NewGuid(); + var asset1 = CreateAsset(assetId1); + var asset2 = CreateAsset(assetId2); + + var query1 = @" + query { + findAsset(id: """") { + id + } + }".Replace("", assetId1.ToString()); + var query2 = @" + query { + findAsset(id: """") { + id + } + }".Replace("", assetId2.ToString()); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId1))) + .Returns(ResultList.CreateFrom(0, asset1)); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId2))) + .Returns(ResultList.CreateFrom(0, asset2)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query1 }, new GraphQLQuery { Query = query2 }); + + var expected = new object[] + { + new + { + data = new + { + findAsset = new + { + id = asset1.Id + } + } + }, + new + { + data = new + { + findAsset = new + { + id = asset2.Id + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_not_return_data_when_field_not_part_of_content() + { + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, Guid.Empty, new NamedContentData()); + + var query = @" + query { + findMySchemaContent(id: """") { + id + version + created + createdBy + lastModified + lastModifiedBy + url + data { + myInvalid { + iv + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var json = serializer.Serialize(result); + + Assert.Contains("\"data\":null", json); + } + + [Fact] + public async Task Should_return_draft_content_when_querying_dataDraft() + { + var dataDraft = new NamedContentData() + .AddField("my-string", + new ContentFieldData() + .AddValue("de", "draft value")) + .AddField("my-number", + new ContentFieldData() + .AddValue("iv", 42)); + + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, Guid.Empty, null, dataDraft); + + var query = @" + query { + findMySchemaContent(id: """") { + dataDraft { + myString { + de + } + myNumber { + iv + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + dataDraft = new + { + myString = new + { + de = "draft value" + }, + myNumber = new + { + iv = 42 + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_null_when_querying_dataDraft_and_no_draft_content_is_available() + { + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, Guid.Empty, null); + + var query = @" + query { + findMySchemaContent(id: """") { + dataDraft { + myString { + de + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + dataDraft = (object?)null + } + } + }; + + AssertResult(expected, result); + } + + private static IReadOnlyList MatchId(Guid contentId) + { + return A>.That.Matches(x => x.Count == 1 && x[0] == contentId); + } + + private static Q MatchIdQuery(Guid contentId) + { + return A.That.Matches(x => x.Ids.Count == 1 && x.Ids[0] == contentId); + } + + private Context MatchsAssetContext() + { + return A.That.Matches(x => x.App == app && x.User == requestContext.User); + } + + private Context MatchsContentContext() + { + return A.That.Matches(x => x.App == app && x.User == requestContext.User); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs new file mode 100644 index 000000000..c188160f9 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -0,0 +1,287 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using FakeItEasy; +using GraphQL; +using GraphQL.DataLoader; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using NodaTime; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.TestData; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Log; +using Xunit; + +#pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public class GraphQLTestBase + { + protected readonly IAppEntity app; + protected readonly IAssetQueryService assetQuery = A.Fake(); + protected readonly IContentQueryService contentQuery = A.Fake(); + protected readonly IJsonSerializer serializer = TestUtils.CreateSerializer(TypeNameHandling.None); + protected readonly ISchemaEntity schema; + protected readonly ISchemaEntity schemaRef1; + protected readonly ISchemaEntity schemaRef2; + protected readonly Context requestContext; + protected readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + protected readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + protected readonly NamedId schemaRefId1 = NamedId.Of(Guid.NewGuid(), "my-ref-schema1"); + protected readonly NamedId schemaRefId2 = NamedId.Of(Guid.NewGuid(), "my-ref-schema2"); + protected readonly IGraphQLService sut; + + public GraphQLTestBase() + { + app = Mocks.App(appId, Language.DE, Language.GermanGermany); + + var schemaDef = + new Schema(schemaId.Name) + .Publish() + .AddJson(1, "my-json", Partitioning.Invariant, + new JsonFieldProperties()) + .AddString(2, "my-string", Partitioning.Language, + new StringFieldProperties()) + .AddNumber(3, "my-number", Partitioning.Invariant, + new NumberFieldProperties()) + .AddNumber(4, "my_number", Partitioning.Invariant, + new NumberFieldProperties()) + .AddAssets(5, "my-assets", Partitioning.Invariant, + new AssetsFieldProperties()) + .AddBoolean(6, "my-boolean", Partitioning.Invariant, + new BooleanFieldProperties()) + .AddDateTime(7, "my-datetime", Partitioning.Invariant, + new DateTimeFieldProperties()) + .AddReferences(8, "my-references", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaRefId1.Id }) + .AddReferences(81, "my-union", Partitioning.Invariant, + new ReferencesFieldProperties()) + .AddReferences(9, "my-invalid", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = Guid.NewGuid() }) + .AddGeolocation(10, "my-geolocation", Partitioning.Invariant, + new GeolocationFieldProperties()) + .AddTags(11, "my-tags", Partitioning.Invariant, + new TagsFieldProperties()) + .AddString(12, "my-localized", Partitioning.Language, + new StringFieldProperties()) + .AddArray(13, "my-array", Partitioning.Invariant, f => f + .AddBoolean(121, "nested-boolean") + .AddNumber(122, "nested-number") + .AddNumber(123, "nested_number")) + .ConfigureScripts(new SchemaScripts { Query = "" }); + + schema = Mocks.Schema(appId, schemaId, schemaDef); + + var schemaRef1Def = + new Schema(schemaRefId1.Name) + .Publish() + .AddString(1, "ref1-field", Partitioning.Invariant); + + schemaRef1 = Mocks.Schema(appId, schemaRefId1, schemaRef1Def); + + var schemaRef2Def = + new Schema(schemaRefId2.Name) + .Publish() + .AddString(1, "ref2-field", Partitioning.Invariant); + + schemaRef2 = Mocks.Schema(appId, schemaRefId2, schemaRef2Def); + + requestContext = new Context(Mocks.FrontendUser(), app); + + sut = CreateSut(); + } + + protected IEnrichedContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData? data = null, NamedContentData? dataDraft = null) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + data ??= + new NamedContentData() + .AddField("my-string", + new ContentFieldData() + .AddValue("de", "value")) + .AddField("my-assets", + new ContentFieldData() + .AddValue("iv", JsonValue.Array(assetId.ToString()))) + .AddField("my-number", + new ContentFieldData() + .AddValue("iv", 1.0)) + .AddField("my_number", + new ContentFieldData() + .AddValue("iv", 2.0)) + .AddField("my-boolean", + new ContentFieldData() + .AddValue("iv", true)) + .AddField("my-datetime", + new ContentFieldData() + .AddValue("iv", now)) + .AddField("my-tags", + new ContentFieldData() + .AddValue("iv", JsonValue.Array("tag1", "tag2"))) + .AddField("my-references", + new ContentFieldData() + .AddValue("iv", JsonValue.Array(refId.ToString()))) + .AddField("my-union", + new ContentFieldData() + .AddValue("iv", JsonValue.Array(refId.ToString()))) + .AddField("my-geolocation", + new ContentFieldData() + .AddValue("iv", JsonValue.Object().Add("latitude", 10).Add("longitude", 20))) + .AddField("my-json", + new ContentFieldData() + .AddValue("iv", JsonValue.Object().Add("value", 1))) + .AddField("my-localized", + new ContentFieldData() + .AddValue("de-DE", "de-DE")) + .AddField("my-array", + new ContentFieldData() + .AddValue("iv", JsonValue.Array( + JsonValue.Object() + .Add("nested-boolean", true) + .Add("nested-number", 10) + .Add("nested_number", 11), + JsonValue.Object() + .Add("nested-boolean", false) + .Add("nested-number", 20) + .Add("nested_number", 21)))); + + var content = new ContentEntity + { + Id = id, + Version = 1, + Created = now, + CreatedBy = new RefToken(RefTokenType.Subject, "user1"), + LastModified = now, + LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), + Data = data, + DataDraft = dataDraft!, + SchemaId = schemaId, + Status = Status.Draft, + StatusColor = "red" + }; + + return content; + } + + protected static IEnrichedContentEntity CreateRefContent(NamedId schemaId, Guid id, string field, string value) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + var data = + new NamedContentData() + .AddField(field, + new ContentFieldData() + .AddValue("iv", value)); + + var content = new ContentEntity + { + Id = id, + Version = 1, + Created = now, + CreatedBy = new RefToken(RefTokenType.Subject, "user1"), + LastModified = now, + LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), + Data = data, + DataDraft = data, + SchemaId = schemaId, + Status = Status.Draft, + StatusColor = "red" + }; + + return content; + } + + protected static IEnrichedAssetEntity CreateAsset(Guid id) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + var asset = new AssetEntity + { + Id = id, + Version = 1, + Created = now, + CreatedBy = new RefToken(RefTokenType.Subject, "user1"), + LastModified = now, + LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), + FileName = "MyFile.png", + Slug = "myfile.png", + FileSize = 1024, + FileHash = "ABC123", + FileVersion = 123, + MimeType = "image/png", + IsImage = true, + PixelWidth = 800, + PixelHeight = 600, + TagNames = new[] { "tag1", "tag2" }.ToHashSet() + }; + + return asset; + } + + protected void AssertResult(object expected, (bool HasErrors, object Response) result, bool checkErrors = true) + { + if (checkErrors && result.HasErrors) + { + throw new InvalidOperationException(Serialize(result)); + } + + var resultJson = serializer.Serialize(result.Response, true); + var expectJson = serializer.Serialize(expected, true); + + Assert.Equal(expectJson, resultJson); + } + + private string Serialize((bool HasErrors, object Response) result) + { + return serializer.Serialize(result); + } + + private CachingGraphQLService CreateSut() + { + var appProvider = A.Fake(); + + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id)) + .Returns(new List { schema, schemaRef1, schemaRef2 }); + + var dataLoaderContext = new DataLoaderContextAccessor(); + + var services = new Dictionary + { + [typeof(IAppProvider)] = appProvider, + [typeof(IAssetQueryService)] = assetQuery, + [typeof(IContentQueryService)] = contentQuery, + [typeof(IDataLoaderContextAccessor)] = dataLoaderContext, + [typeof(IGraphQLUrlGenerator)] = new FakeUrlGenerator(), + [typeof(IOptions)] = Options.Create(new AssetOptions()), + [typeof(IOptions)] = Options.Create(new ContentOptions()), + [typeof(ISemanticLog)] = A.Fake(), + [typeof(DataLoaderDocumentListener)] = new DataLoaderDocumentListener(dataLoaderContext) + }; + + var resolver = new FuncDependencyResolver(t => services[t]); + + var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + + return new CachingGraphQLService(cache, resolver); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs new file mode 100644 index 000000000..7d603ae59 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs @@ -0,0 +1,289 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using FakeItEasy; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using NodaTime.Text; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.MongoDb.Contents; +using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.MongoDb.Queries; +using Squidex.Infrastructure.Queries; +using Xunit; +using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; +using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; + +namespace Squidex.Domain.Apps.Entities.Contents.MongoDb +{ + public class MongoDbQueryTests + { + private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; + private static readonly IBsonSerializer Serializer = BsonSerializer.SerializerRegistry.GetSerializer(); + private readonly Schema schemaDef; + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); + + static MongoDbQueryTests() + { + InstantSerializer.Register(); + } + + public MongoDbQueryTests() + { + schemaDef = + new Schema("user") + .AddString(1, "firstName", Partitioning.Language, + new StringFieldProperties()) + .AddString(2, "lastName", Partitioning.Language, + new StringFieldProperties()) + .AddBoolean(3, "isAdmin", Partitioning.Invariant, + new BooleanFieldProperties()) + .AddNumber(4, "age", Partitioning.Invariant, + new NumberFieldProperties()) + .AddDateTime(5, "birthday", Partitioning.Invariant, + new DateTimeFieldProperties()) + .AddAssets(6, "pictures", Partitioning.Invariant, + new AssetsFieldProperties()) + .AddReferences(7, "friends", Partitioning.Invariant, + new ReferencesFieldProperties()) + .AddString(8, "dashed-field", Partitioning.Invariant, + new StringFieldProperties()) + .AddArray(9, "hobbies", Partitioning.Invariant, a => a + .AddString(91, "name")) + .Update(new SchemaProperties()); + + var schema = A.Dummy(); + A.CallTo(() => schema.Id).Returns(Guid.NewGuid()); + A.CallTo(() => schema.Version).Returns(3); + A.CallTo(() => schema.SchemaDef).Returns(schemaDef); + + var app = A.Dummy(); + A.CallTo(() => app.Id).Returns(Guid.NewGuid()); + A.CallTo(() => app.Version).Returns(3); + A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); + } + + [Fact] + public void Should_throw_exception_for_invalid_field() + { + Assert.Throws(() => F(ClrFilter.Eq("data/invalid/iv", "Me"))); + } + + [Fact] + public void Should_make_query_with_lastModified() + { + var i = F(ClrFilter.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); + var o = C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_lastModifiedBy() + { + var i = F(ClrFilter.Eq("lastModifiedBy", "Me")); + var o = C("{ 'mb' : 'Me' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_created() + { + var i = F(ClrFilter.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); + var o = C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_createdBy() + { + var i = F(ClrFilter.Eq("createdBy", "Me")); + var o = C("{ 'cb' : 'Me' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_version() + { + var i = F(ClrFilter.Eq("version", 0L)); + var o = C("{ 'vs' : NumberLong(0) }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_version_and_list() + { + var i = F(ClrFilter.In("version", new List { 0L, 2L, 5L })); + var o = C("{ 'vs' : { '$in' : [NumberLong(0), NumberLong(2), NumberLong(5)] } }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_from_draft() + { + var i = F(ClrFilter.Eq("data/dashed_field/iv", "Value"), true); + var o = C("{ 'dd.8.iv' : 'Value' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_empty_test() + { + var i = F(ClrFilter.Empty("data/firstName/iv"), true); + var o = C("{ '$or' : [{ 'dd.1.iv' : { '$exists' : false } }, { 'dd.1.iv' : null }, { 'dd.1.iv' : '' }, { 'dd.1.iv' : [] }] }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_datetime_data() + { + var i = F(ClrFilter.Eq("data/birthday/iv", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); + var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_underscore_field() + { + var i = F(ClrFilter.Eq("data/dashed_field/iv", "Value")); + var o = C("{ 'do.8.iv' : 'Value' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_references_equals() + { + var i = F(ClrFilter.Eq("data/friends/iv", "guid")); + var o = C("{ 'do.7.iv' : 'guid' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_array_field() + { + var i = F(ClrFilter.Eq("data/hobbies/iv/name", "PC")); + var o = C("{ 'do.9.iv.91' : 'PC' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_assets_equals() + { + var i = F(ClrFilter.Eq("data/pictures/iv", "guid")); + var o = C("{ 'do.6.iv' : 'guid' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_full_text() + { + var i = Q(new ClrQuery { FullText = "Hello my World" }); + var o = C("{ '$text' : { '$search' : 'Hello my World' } }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_orderby_with_single_field() + { + var i = S(SortBuilder.Descending("data/age/iv")); + var o = C("{ 'do.4.iv' : -1 }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_orderby_with_multiple_fields() + { + var i = S(SortBuilder.Ascending("data/age/iv"), SortBuilder.Descending("data/firstName/en")); + var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_take_statement() + { + var query = new ClrQuery { Take = 3 }; + var cursor = A.Fake>(); + + cursor.ContentTake(query.AdjustToModel(schemaDef, false)); + + A.CallTo(() => cursor.Limit(3)) + .MustHaveHappened(); + } + + [Fact] + public void Should_make_skip_statement() + { + var query = new ClrQuery { Skip = 3 }; + var cursor = A.Fake>(); + + cursor.ContentSkip(query.AdjustToModel(schemaDef, false)); + + A.CallTo(() => cursor.Skip(3)) + .MustHaveHappened(); + } + + private static string C(string value) + { + return value.Replace('\'', '"'); + } + + private string F(FilterNode filter, bool useDraft = false) + { + return Q(new ClrQuery { Filter = filter }, useDraft); + } + + private string S(params SortNode[] sorts) + { + var cursor = A.Fake>(); + + var i = string.Empty; + + A.CallTo(() => cursor.Sort(A>.Ignored)) + .Invokes((SortDefinition sortDefinition) => + { + i = sortDefinition.Render(Serializer, Registry).ToString(); + }); + + cursor.ContentSort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel(schemaDef, false)); + + return i; + } + + private string Q(ClrQuery query, bool useDraft = false) + { + var rendered = + query.AdjustToModel(schemaDef, useDraft).BuildFilter().Filter! + .Render(Serializer, Registry).ToString(); + + return rendered; + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs new file mode 100644 index 000000000..36962b4cd --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs @@ -0,0 +1,204 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class ContentEnricherTests + { + private readonly IContentWorkflow contentWorkflow = A.Fake(); + private readonly IContentQueryService contentQuery = A.Fake(); + private readonly IAssetQueryService assetQuery = A.Fake(); + private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake(); + private readonly ISchemaEntity schema; + private readonly Context requestContext; + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly ContentEnricher sut; + + public ContentEnricherTests() + { + requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); + + schema = Mocks.Schema(appId, schemaId); + + A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(A.Ignored, schemaId.Id.ToString())) + .Returns(schema); + + sut = new ContentEnricher(assetQuery, assetUrlGenerator, new Lazy(() => contentQuery), contentWorkflow); + } + + [Fact] + public async Task Should_add_app_version_and_schema_as_dependency() + { + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.Contains(requestContext.App.Version, result.CacheDependencies); + + Assert.Contains(schema.Id, result.CacheDependencies); + Assert.Contains(schema.Version, result.CacheDependencies); + } + + [Fact] + public async Task Should_enrich_with_reference_fields() + { + var ctx = new Context(Mocks.FrontendUser(), requestContext.App); + + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, ctx); + + Assert.NotNull(result.ReferenceFields); + } + + [Fact] + public async Task Should_not_enrich_with_reference_fields_when_not_frontend() + { + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.Null(result.ReferenceFields); + } + + [Fact] + public async Task Should_enrich_with_schema_names() + { + var ctx = new Context(Mocks.FrontendUser(), requestContext.App); + + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, ctx); + + Assert.Equal("my-schema", result.SchemaName); + Assert.Equal("my-schema", result.SchemaDisplayName); + } + + [Fact] + public async Task Should_not_enrich_with_schema_names_when_not_frontend() + { + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.Null(result.SchemaName); + Assert.Null(result.SchemaDisplayName); + } + + [Fact] + public async Task Should_enrich_content_with_status_color() + { + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.Equal(StatusColors.Published, result.StatusColor); + } + + [Fact] + public async Task Should_enrich_content_with_default_color_if_not_found() + { + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(Task.FromResult(null!)); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.Equal(StatusColors.Draft, result.StatusColor); + } + + [Fact] + public async Task Should_enrich_content_with_can_update() + { + requestContext.WithResolveFlow(true); + + var source = new ContentEntity { SchemaId = schemaId }; + + A.CallTo(() => contentWorkflow.CanUpdateAsync(source)) + .Returns(true); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.True(result.CanUpdate); + } + + [Fact] + public async Task Should_not_enrich_content_with_can_update_if_disabled_in_context() + { + requestContext.WithResolveFlow(false); + + var source = new ContentEntity { SchemaId = schemaId }; + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.False(result.CanUpdate); + + A.CallTo(() => contentWorkflow.CanUpdateAsync(source)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_enrich_multiple_contents_and_cache_color() + { + var source1 = PublishedContent(); + var source2 = PublishedContent(); + + var source = new IContentEntity[] + { + source1, + source2 + }; + + A.CallTo(() => contentWorkflow.GetInfoAsync(source1)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.Equal(StatusColors.Published, result[0].StatusColor); + Assert.Equal(StatusColors.Published, result[1].StatusColor); + + A.CallTo(() => contentWorkflow.GetInfoAsync(A.Ignored)) + .MustHaveHappenedOnceExactly(); + } + + private ContentEntity PublishedContent() + { + return new ContentEntity { Status = Status.Published, SchemaId = schemaId }; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs new file mode 100644 index 000000000..09a1dd703 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class ContentLoaderTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IContentGrain grain = A.Fake(); + private readonly Guid id = Guid.NewGuid(); + private readonly ContentLoader sut; + + public ContentLoaderTests() + { + A.CallTo(() => grainFactory.GetGrain(id, null)) + .Returns(grain); + + sut = new ContentLoader(grainFactory); + } + + [Fact] + public async Task Should_throw_exception_if_no_state_returned() + { + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(null!)); + + await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); + } + + [Fact] + public async Task Should_throw_exception_if_state_has_other_version() + { + var content = new ContentEntity { Version = 5 }; + + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(content)); + + await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); + } + + [Fact] + public async Task Should_not_throw_exception_if_state_has_other_version_than_any() + { + var content = new ContentEntity { Version = 5 }; + + A.CallTo(() => grain.GetStateAsync(EtagVersion.Any)) + .Returns(J.Of(content)); + + await sut.GetAsync(id, EtagVersion.Any); + } + + [Fact] + public async Task Should_return_content_from_state() + { + var content = new ContentEntity { Version = 10 }; + + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(content)); + + var result = await sut.GetAsync(id, 10); + + Assert.Same(content, result); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs new file mode 100644 index 000000000..80a5e9da7 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs @@ -0,0 +1,503 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Shared.Identity; +using Xunit; + +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class ContentQueryServiceTests + { + private readonly IAppEntity app; + private readonly IAppProvider appProvider = A.Fake(); + private readonly IAssetUrlGenerator urlGenerator = A.Fake(); + private readonly IContentEnricher contentEnricher = A.Fake(); + private readonly IContentRepository contentRepository = A.Fake(); + private readonly IContentLoader contentVersionLoader = A.Fake(); + private readonly ISchemaEntity schema; + private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly Guid contentId = Guid.NewGuid(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly NamedContentData contentData = new NamedContentData(); + private readonly NamedContentData contentTransformed = new NamedContentData(); + private readonly ClaimsPrincipal user; + private readonly ClaimsIdentity identity = new ClaimsIdentity(); + private readonly Context requestContext; + private readonly ContentQueryParser queryParser = A.Fake(); + private readonly ContentQueryService sut; + + public static IEnumerable ApiStatusTests = new[] + { + new object?[] { 0, new[] { Status.Published } }, + new object?[] { 1, null } + }; + + public ContentQueryServiceTests() + { + user = new ClaimsPrincipal(identity); + + app = Mocks.App(appId); + + requestContext = new Context(user, app); + + var schemaDef = + new Schema(schemaId.Name) + .ConfigureScripts(new SchemaScripts { Query = "" }); + + schema = Mocks.Schema(appId, schemaId, schemaDef); + + SetupEnricher(); + + A.CallTo(() => queryParser.ParseQuery(requestContext, schema, A.Ignored)) + .Returns(new ClrQuery()); + + sut = new ContentQueryService( + appProvider, + urlGenerator, + contentEnricher, + contentRepository, + contentVersionLoader, + scriptEngine, + queryParser); + } + + [Fact] + public async Task Should_return_schema_from_id_if_string_is_guid() + { + SetupSchemaFound(); + + var result = await sut.GetSchemaOrThrowAsync(requestContext, schemaId.Id.ToString()); + + Assert.Equal(schema, result); + } + + [Fact] + public async Task Should_return_schema_from_name_if_string_not_guid() + { + SetupSchemaFound(); + + var result = await sut.GetSchemaOrThrowAsync(requestContext, schemaId.Name); + + Assert.Equal(schema, result); + } + + [Fact] + public async Task Should_throw_404_if_schema_not_found() + { + SetupSchemaNotFound(); + + var ctx = requestContext; + + await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name)); + } + + [Fact] + public async Task Should_throw_404_if_schema_not_found_in_check() + { + SetupSchemaNotFound(); + + var ctx = requestContext; + + await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name)); + } + + [Fact] + public async Task Should_throw_for_single_content_if_no_permission() + { + SetupUser(false, false); + SetupSchemaFound(); + + var ctx = requestContext; + + await Assert.ThrowsAsync(() => sut.FindContentAsync(ctx, schemaId.Name, contentId)); + } + + [Fact] + public async Task Should_throw_404_for_single_content_if_content_not_found() + { + var status = new[] { Status.Published }; + + SetupUser(isFrontend: false); + SetupSchemaFound(); + SetupContent(status, null, includeDraft: false); + + var ctx = requestContext; + + await Assert.ThrowsAsync(async () => await sut.FindContentAsync(ctx, schemaId.Name, contentId)); + } + + [Fact] + public async Task Should_return_single_content_for_frontend_without_transform() + { + var content = CreateContent(contentId); + + SetupUser(isFrontend: true); + SetupSchemaFound(); + SetupSchemaScripting(contentId); + SetupContent(null, content, includeDraft: true); + + var ctx = requestContext; + + var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId); + + Assert.Equal(contentTransformed, result!.Data); + Assert.Equal(content.Id, result.Id); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Theory] + [MemberData(nameof(ApiStatusTests))] + public async Task Should_return_single_content_for_api_with_transform(int unpublished, Status[] status) + { + var content = CreateContent(contentId); + + SetupUser(isFrontend: false); + SetupSchemaFound(); + SetupSchemaScripting(contentId); + SetupContent(status, content, unpublished == 1); + + var ctx = requestContext.WithUnpublished(unpublished == 1); + + var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId); + + Assert.Equal(contentTransformed, result!.Data); + Assert.Equal(content.Id, result.Id); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_return_versioned_content_from_repository_and_transform() + { + var content = CreateContent(contentId); + + SetupUser(true); + SetupSchemaFound(); + SetupSchemaScripting(contentId); + + A.CallTo(() => contentVersionLoader.GetAsync(contentId, 10)) + .Returns(content); + + var ctx = requestContext; + + var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId, 10); + + Assert.Equal(contentTransformed, result!.Data); + Assert.Equal(content.Id, result.Id); + } + + [Fact] + public async Task Should_throw_for_query_if_no_permission() + { + SetupUser(false, false); + SetupSchemaFound(); + + var ctx = requestContext; + + await Assert.ThrowsAsync(() => sut.QueryAsync(ctx, schemaId.Name, Q.Empty)); + } + + [Fact] + public async Task Should_query_contents_by_query_for_frontend_without_transform() + { + const int count = 5, total = 200; + + var content = CreateContent(contentId); + + SetupUser(isFrontend: true); + SetupSchemaFound(); + SetupSchemaScripting(contentId); + SetupContents(null, count, total, content, inDraft: true, includeDraft: true); + + var ctx = requestContext; + + var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty); + + Assert.Equal(contentData, result[0].Data); + Assert.Equal(content.Id, result[0].Id); + + Assert.Equal(total, result.Total); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Theory] + [MemberData(nameof(ApiStatusTests))] + public async Task Should_query_contents_by_query_for_api_and_transform(int unpublished, Status[] status) + { + const int count = 5, total = 200; + + var content = CreateContent(contentId); + + SetupUser(isFrontend: false); + SetupSchemaFound(); + SetupSchemaScripting(contentId); + SetupContents(status, count, total, content, inDraft: false, unpublished == 1); + + var ctx = requestContext.WithUnpublished(unpublished == 1); + + var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty); + + Assert.Equal(contentData, result[0].Data); + Assert.Equal(contentId, result[0].Id); + + Assert.Equal(total, result.Total); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustHaveHappened(count, Times.Exactly); + } + + [Fact] + public async Task Should_query_contents_by_id_for_frontend_and_transform() + { + const int count = 5, total = 200; + + var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); + + SetupUser(isFrontend: true); + SetupSchemaFound(); + SetupSchemaScripting(ids.ToArray()); + SetupContents(null, total, ids, includeDraft: true); + + var ctx = requestContext; + + var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty.WithIds(ids)); + + Assert.Equal(ids, result.Select(x => x.Id).ToList()); + Assert.Equal(total, result.Total); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Theory] + [MemberData(nameof(ApiStatusTests))] + public async Task Should_query_contents_by_id_for_api_and_transform(int unpublished, Status[] status) + { + const int count = 5, total = 200; + + var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); + + SetupUser(isFrontend: false); + SetupSchemaFound(); + SetupSchemaScripting(ids.ToArray()); + SetupContents(status, total, ids, unpublished == 1); + + var ctx = requestContext.WithUnpublished(unpublished == 1); + + var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty.WithIds(ids)); + + Assert.Equal(ids, result.Select(x => x.Id).ToList()); + Assert.Equal(total, result.Total); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustHaveHappened(count, Times.Exactly); + } + + [Fact] + public async Task Should_query_all_contents_by_id_for_frontend_and_transform() + { + const int count = 5; + + var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); + + SetupUser(isFrontend: true); + SetupSchemaFound(); + SetupSchemaScripting(ids.ToArray()); + SetupContents(null, ids, includeDraft: true); + + var ctx = requestContext; + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Equal(ids, result.Select(x => x.Id).ToList()); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Theory] + [MemberData(nameof(ApiStatusTests))] + public async Task Should_query_all_contents_by_id_for_api_and_transform(int unpublished, Status[] status) + { + const int count = 5; + + var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); + + SetupUser(isFrontend: false); + SetupSchemaFound(); + SetupSchemaScripting(ids.ToArray()); + SetupContents(status, ids, unpublished == 1); + + var ctx = requestContext.WithUnpublished(unpublished == 1); + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Equal(ids, result.Select(x => x.Id).ToList()); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustHaveHappened(count, Times.Exactly); + } + + [Fact] + public async Task Should_skip_contents_when_user_has_no_permission() + { + var ids = Enumerable.Range(0, 1).Select(x => Guid.NewGuid()).ToList(); + + SetupUser(isFrontend: false, allowSchema: false); + SetupSchemaFound(); + SetupSchemaScripting(ids.ToArray()); + SetupContents(new Status[0], ids, includeDraft: false); + + var ctx = requestContext; + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Empty(result); + } + + [Fact] + public async Task Should_not_call_repository_if_no_id_defined() + { + var ids = new List(); + + SetupUser(isFrontend: false, allowSchema: false); + SetupSchemaFound(); + + var ctx = requestContext; + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Empty(result); + + A.CallTo(() => contentRepository.QueryAsync(app, A.Ignored, A>.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + private void SetupUser(bool isFrontend, bool allowSchema = true) + { + if (isFrontend) + { + identity.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); + } + + if (allowSchema) + { + identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.ForApp(Permissions.AppContentsRead, app.Name, schema.SchemaDef.Name).Id)); + } + + requestContext.UpdatePermissions(); + } + + private void SetupSchemaScripting(params Guid[] ids) + { + foreach (var id in ids) + { + A.CallTo(() => scriptEngine.Transform(A.That.Matches(x => x.User == user && x.ContentId == id && x.Data == contentData), "")) + .Returns(contentTransformed); + } + } + + private void SetupContents(Status[]? status, int count, int total, IContentEntity content, bool inDraft, bool includeDraft) + { + A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.Is(status), inDraft, A.Ignored, includeDraft)) + .Returns(ResultList.Create(total, Enumerable.Repeat(content, count))); + } + + private void SetupContents(Status[]? status, int total, List ids, bool includeDraft) + { + A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.Is(status), A>.Ignored, includeDraft)) + .Returns(ResultList.Create(total, ids.Select(CreateContent).Shuffle())); + } + + private void SetupContents(Status[]? status, List ids, bool includeDraft) + { + A.CallTo(() => contentRepository.QueryAsync(app, A.That.Is(status), A>.Ignored, includeDraft)) + .Returns(ids.Select(x => (CreateContent(x), schema)).ToList()); + } + + private void SetupContent(Status[]? status, IContentEntity? content, bool includeDraft) + { + A.CallTo(() => contentRepository.FindContentAsync(app, schema, A.That.Is(status), contentId, includeDraft)) + .Returns(content); + } + + private void SetupSchemaFound() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name)) + .Returns(schema); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + .Returns(schema); + } + + private void SetupSchemaNotFound() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name)) + .Returns((ISchemaEntity?)null); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + .Returns((ISchemaEntity?)null); + } + + private void SetupEnricher() + { + A.CallTo(() => contentEnricher.EnrichAsync(A>.Ignored, requestContext)) + .ReturnsLazily(x => + { + var input = (IEnumerable)x.Arguments[0]; + + return Task.FromResult>(input.Select(c => SimpleMapper.Map(c, new ContentEntity())).ToList()); + }); + } + + private IContentEntity CreateContent(Guid id) + { + return CreateContent(id, Status.Published); + } + + private IContentEntity CreateContent(Guid id, Status status) + { + var content = new ContentEntity + { + Id = id, + Data = contentData, + DataDraft = contentData, + SchemaId = schemaId, + Status = status + }; + + return content; + } + } +} \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs new file mode 100644 index 000000000..05ee89c0a --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs @@ -0,0 +1,105 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using FakeItEasy; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class FilterTagTransformerTests + { + private readonly ITagService tagService = A.Fake(); + private readonly ISchemaEntity schema; + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + + public FilterTagTransformerTests() + { + var schemaDef = + new Schema("schema") + .AddTags(1, "tags1", Partitioning.Invariant) + .AddTags(2, "tags2", Partitioning.Invariant, new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) + .AddString(3, "string", Partitioning.Invariant); + + schema = Mocks.Schema(appId, schemaId, schemaDef); + } + + [Fact] + public void Should_normalize_tags() + { + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Schemas(schemaId.Id), A>.That.Contains("name1"))) + .Returns(new Dictionary { ["name1"] = "id1" }); + + var source = ClrFilter.Eq("data.tags2.iv", "name1"); + + var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); + + Assert.Equal("data.tags2.iv == 'id1'", result!.ToString()); + } + + [Fact] + public void Should_not_fail_when_tags_not_found() + { + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A>.That.Contains("name1"))) + .Returns(new Dictionary()); + + var source = ClrFilter.Eq("data.tags2.iv", "name1"); + + var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); + + Assert.Equal("data.tags2.iv == 'name1'", result!.ToString()); + } + + [Fact] + public void Should_not_normalize_other_tags_field() + { + var source = ClrFilter.Eq("data.tags1.iv", "value"); + + var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); + + Assert.Equal("data.tags1.iv == 'value'", result!.ToString()); + + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A.Ignored, A>.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public void Should_not_normalize_other_typed_field() + { + var source = ClrFilter.Eq("data.string.iv", "value"); + + var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); + + Assert.Equal("data.string.iv == 'value'", result!.ToString()); + + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A.Ignored, A>.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public void Should_not_normalize_non_data_field() + { + var source = ClrFilter.Eq("no.data", "value"); + + var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); + + Assert.Equal("no.data == 'value'", result!.ToString()); + + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A.Ignored, A>.Ignored)) + .MustNotHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/GrainTextIndexerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/GrainTextIndexerTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/GrainTextIndexerTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/GrainTextIndexerTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerGrainTests.cs new file mode 100644 index 000000000..f370f6190 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerGrainTests.cs @@ -0,0 +1,263 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public class TextIndexerGrainTests : IDisposable + { + private readonly Guid schemaId = Guid.NewGuid(); + private readonly List ids1 = new List { Guid.NewGuid() }; + private readonly List ids2 = new List { Guid.NewGuid() }; + private readonly SearchContext context; + private readonly IAssetStore assetStore = new MemoryAssetStore(); + private readonly TextIndexerGrain sut; + + public TextIndexerGrainTests() + { + context = new SearchContext + { + Languages = new HashSet { "de", "en" } + }; + + sut = new TextIndexerGrain(assetStore); + sut.ActivateAsync(schemaId).Wait(); + } + + public void Dispose() + { + sut.OnDeactivateAsync().Wait(); + } + + [Fact] + public async Task Should_throw_exception_for_invalid_query() + { + await Assert.ThrowsAsync(() => sut.SearchAsync("~hello", context)); + } + + [Fact] + public async Task Should_read_index_and_retrieve() + { + await AddInvariantContent("Hello", "World", false); + + await sut.DeactivateAsync(true); + + var other = new TextIndexerGrain(assetStore); + try + { + await other.ActivateAsync(schemaId); + + await TestSearchAsync(ids1, "Hello", grain: other); + await TestSearchAsync(ids2, "World", grain: other); + } + finally + { + await other.OnDeactivateAsync(); + } + } + + [Fact] + public async Task Should_index_invariant_content_and_retrieve() + { + await AddInvariantContent("Hello", "World", false); + + await TestSearchAsync(ids1, "Hello"); + await TestSearchAsync(ids2, "World"); + } + + [Fact] + public async Task Should_index_invariant_content_and_retrieve_with_fuzzy() + { + await AddInvariantContent("Hello", "World", false); + + await TestSearchAsync(ids1, "helo~"); + await TestSearchAsync(ids2, "wold~"); + } + + [Fact] + public async Task Should_update_draft_only() + { + await AddInvariantContent("Hello", "World", false); + await AddInvariantContent("Hallo", "Welt", false); + + await TestSearchAsync(null, "Hello", Scope.Draft); + await TestSearchAsync(null, "Hello", Scope.Published); + + await TestSearchAsync(ids1, "Hallo", Scope.Draft); + await TestSearchAsync(null, "Hallo", Scope.Published); + } + + [Fact] + public async Task Should_also_update_published_after_copy() + { + await AddInvariantContent("Hello", "World", false); + + await CopyAsync(true); + + await AddInvariantContent("Hallo", "Welt", false); + + await TestSearchAsync(null, "Hello", Scope.Draft); + await TestSearchAsync(null, "Hello", Scope.Published); + + await TestSearchAsync(ids1, "Hallo", Scope.Draft); + await TestSearchAsync(ids1, "Hallo", Scope.Published); + } + + [Fact] + public async Task Should_simulate_content_reversion() + { + await AddInvariantContent("Hello", "World", false); + + await CopyAsync(true); + + await AddInvariantContent("Hallo", "Welt", true); + + await TestSearchAsync(null, "Hello", Scope.Draft); + await TestSearchAsync(ids1, "Hello", Scope.Published); + + await TestSearchAsync(ids1, "Hallo", Scope.Draft); + await TestSearchAsync(null, "Hallo", Scope.Published); + + await CopyAsync(false); + + await TestSearchAsync(ids1, "Hello", Scope.Draft); + await TestSearchAsync(ids1, "Hello", Scope.Published); + + await TestSearchAsync(null, "Hallo", Scope.Draft); + await TestSearchAsync(null, "Hallo", Scope.Published); + + await AddInvariantContent("Guten Morgen", "Welt", true); + + await TestSearchAsync(null, "Hello", Scope.Draft); + await TestSearchAsync(ids1, "Hello", Scope.Published); + + await TestSearchAsync(ids1, "Guten Morgen", Scope.Draft); + await TestSearchAsync(null, "Guten Morgen", Scope.Published); + } + + [Fact] + public async Task Should_also_retrieve_published_content_after_copy() + { + await AddInvariantContent("Hello", "World", false); + + await TestSearchAsync(ids1, "Hello", Scope.Draft); + await TestSearchAsync(null, "Hello", Scope.Published); + + await CopyAsync(true); + + await TestSearchAsync(ids1, "Hello", Scope.Draft); + await TestSearchAsync(ids1, "Hello", Scope.Published); + } + + [Fact] + public async Task Should_delete_documents_from_index() + { + await AddInvariantContent("Hello", "World", false); + + await TestSearchAsync(ids1, "Hello"); + await TestSearchAsync(ids2, "World"); + + await DeleteAsync(ids1[0]); + + await TestSearchAsync(null, "Hello"); + await TestSearchAsync(ids2, "World"); + } + + [Fact] + public async Task Should_search_by_field() + { + await AddLocalizedContent(); + + await TestSearchAsync(null, "de:city"); + await TestSearchAsync(null, "en:Stadt"); + } + + [Fact] + public async Task Should_index_localized_content_and_retrieve() + { + await AddLocalizedContent(); + + await TestSearchAsync(ids1, "Stadt"); + await TestSearchAsync(ids1, "and"); + await TestSearchAsync(ids2, "und"); + + await TestSearchAsync(ids2, "City"); + await TestSearchAsync(ids2, "und"); + await TestSearchAsync(ids1, "and"); + } + + private async Task AddLocalizedContent() + { + var germanData = + new NamedContentData() + .AddField("localized", + new ContentFieldData() + .AddValue("de", "Stadt und Umgebung and whatever")); + + var englishData = + new NamedContentData() + .AddField("localized", + new ContentFieldData() + .AddValue("en", "City and Surroundings und sonstiges")); + + await sut.IndexAsync(new Update { Id = ids1[0], Data = germanData, OnlyDraft = true }); + await sut.IndexAsync(new Update { Id = ids2[0], Data = englishData, OnlyDraft = true }); + } + + private async Task AddInvariantContent(string text1, string text2, bool onlyDraft = false) + { + var data1 = + new NamedContentData() + .AddField("test", + new ContentFieldData() + .AddValue("iv", text1)); + + var data2 = + new NamedContentData() + .AddField("test", + new ContentFieldData() + .AddValue("iv", text2)); + + await sut.IndexAsync(new Update { Id = ids1[0], Data = data1, OnlyDraft = onlyDraft }); + await sut.IndexAsync(new Update { Id = ids2[0], Data = data2, OnlyDraft = onlyDraft }); + } + + private async Task DeleteAsync(Guid id) + { + await sut.DeleteAsync(id); + } + + private async Task CopyAsync(bool fromDraft) + { + await sut.CopyAsync(ids1[0], fromDraft); + await sut.CopyAsync(ids2[0], fromDraft); + } + + private async Task TestSearchAsync(List? expected, string text, Scope target = Scope.Draft, TextIndexerGrain? grain = null) + { + context.Scope = target; + + var result = await (grain ?? sut).SearchAsync(text, context); + + if (expected != null) + { + Assert.Equal(expected, result); + } + else + { + Assert.Empty(result); + } + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs new file mode 100644 index 000000000..be219adaa --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs @@ -0,0 +1,191 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using NodaTime; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.History.Notifications +{ + public class NotificationEmailEventConsumerTests + { + private readonly INotificationEmailSender emailSender = A.Fake(); + private readonly IUserResolver userResolver = A.Fake(); + private readonly IUser assigner = A.Fake(); + private readonly IUser assignee = A.Fake(); + private readonly ISemanticLog log = A.Fake(); + private readonly string assignerId = Guid.NewGuid().ToString(); + private readonly string assigneeId = Guid.NewGuid().ToString(); + private readonly string appName = "my-app"; + private readonly NotificationEmailEventConsumer sut; + + public NotificationEmailEventConsumerTests() + { + A.CallTo(() => emailSender.IsActive) + .Returns(true); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId)) + .Returns(assigner); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId)) + .Returns(assignee); + + sut = new NotificationEmailEventConsumer(emailSender, userResolver, log); + } + + [Fact] + public async Task Should_not_send_email_if_contributors_assigned_by_clients() + { + var @event = CreateEvent(RefTokenType.Client, true); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_email_for_initial_owner() + { + var @event = CreateEvent(RefTokenType.Subject, false, streamNumber: 1); + + await sut.On(@event); + + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_email_for_old_events() + { + var @event = CreateEvent(RefTokenType.Subject, true, instant: SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(50))); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_email_for_old_contributor() + { + var @event = CreateEvent(RefTokenType.Subject, true, isNewContributor: false); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_email_if_sender_not_active() + { + var @event = CreateEvent(RefTokenType.Subject, true); + + A.CallTo(() => emailSender.IsActive) + .Returns(false); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_email_if_assigner_not_found() + { + var @event = CreateEvent(RefTokenType.Subject, true); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId)) + .Returns(Task.FromResult(null)); + + await sut.On(@event); + + MustNotSendEmail(); + MustLogWarning(); + } + + [Fact] + public async Task Should_not_send_email_if_assignee_not_found() + { + var @event = CreateEvent(RefTokenType.Subject, true); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId)) + .Returns(Task.FromResult(null)); + + await sut.On(@event); + + MustNotSendEmail(); + MustLogWarning(); + } + + [Fact] + public async Task Should_send_email_for_new_user() + { + var @event = CreateEvent(RefTokenType.Subject, true); + + await sut.On(@event); + + A.CallTo(() => emailSender.SendContributorEmailAsync(assigner, assignee, appName, true)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_send_email_for_existing_user() + { + var @event = CreateEvent(RefTokenType.Subject, false); + + await sut.On(@event); + + A.CallTo(() => emailSender.SendContributorEmailAsync(assigner, assignee, appName, false)) + .MustHaveHappened(); + } + + private void MustLogWarning() + { + A.CallTo(() => log.Log(SemanticLogLevel.Warning, A.Ignored, A>.Ignored)) + .MustHaveHappened(); + } + + private void MustNotResolveUser() + { + A.CallTo(() => userResolver.FindByIdOrEmailAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + private void MustNotSendEmail() + { + A.CallTo(() => emailSender.SendContributorEmailAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + private Envelope CreateEvent(string assignerType, bool isNewUser, bool isNewContributor = true, Instant? instant = null, int streamNumber = 2) + { + var @event = new AppContributorAssigned + { + Actor = new RefToken(assignerType, assignerId), + AppId = NamedId.Of(Guid.NewGuid(), appName), + ContributorId = assigneeId, + IsCreated = isNewUser, + IsAdded = isNewContributor + }; + + var envelope = Envelope.Create(@event); + + envelope.SetTimestamp(instant ?? SystemClock.Instance.GetCurrentInstant()); + envelope.SetEventStreamNumber(streamNumber); + + return envelope; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailSenderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailSenderTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailSenderTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailSenderTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs new file mode 100644 index 000000000..8883862db --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs @@ -0,0 +1,188 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Validation; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Rules.Guards +{ + public class GuardRuleTests + { + private readonly Uri validUrl = new Uri("https://squidex.io"); + private readonly Rule rule_0 = new Rule(new ContentChangedTriggerV2(), new TestAction()).Rename("MyName"); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly IAppProvider appProvider = A.Fake(); + + public sealed class TestAction : RuleAction + { + public Uri Url { get; set; } + } + + public GuardRuleTests() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + .Returns(Mocks.Schema(appId, schemaId)); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_trigger_null() + { + var command = CreateCommand(new CreateRule + { + Trigger = null!, + Action = new TestAction + { + Url = validUrl + } + }); + + await ValidationAssert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider), + new ValidationError("Trigger is required.", "Trigger")); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_action_null() + { + var command = CreateCommand(new CreateRule + { + Trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Empty() + }, + Action = null! + }); + + await ValidationAssert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider), + new ValidationError("Action is required.", "Action")); + } + + [Fact] + public async Task CanCreate_should_not_throw_exception_if_trigger_and_action_valid() + { + var command = CreateCommand(new CreateRule + { + Trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Empty() + }, + Action = new TestAction + { + Url = validUrl + } + }); + + await GuardRule.CanCreate(command, appProvider); + } + + [Fact] + public async Task CanUpdate_should_throw_exception_if_action_and_trigger_are_null() + { + var command = new UpdateRule(); + + await ValidationAssert.ThrowsAsync(() => GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0), + new ValidationError("Either trigger, action or name is required.", "Trigger", "Action")); + } + + [Fact] + public async Task CanUpdate_should_throw_exception_if_rule_has_already_this_name() + { + var command = new UpdateRule + { + Name = "MyName" + }; + + await ValidationAssert.ThrowsAsync(() => GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0), + new ValidationError("Rule has already this name.", "Name")); + } + + [Fact] + public async Task CanUpdate_should_not_throw_exception_if_trigger_action__and_name_are_valid() + { + var command = new UpdateRule + { + Trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Empty() + }, + Action = new TestAction + { + Url = validUrl + }, + Name = "NewName" + }; + + await GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0); + } + + [Fact] + public void CanEnable_should_throw_exception_if_rule_enabled() + { + var command = new EnableRule(); + + var rule_1 = rule_0.Enable(); + + Assert.Throws(() => GuardRule.CanEnable(command, rule_1)); + } + + [Fact] + public void CanEnable_should_not_throw_exception_if_rule_disabled() + { + var command = new EnableRule(); + + var rule_1 = rule_0.Disable(); + + GuardRule.CanEnable(command, rule_1); + } + + [Fact] + public void CanDisable_should_throw_exception_if_rule_disabled() + { + var command = new DisableRule(); + + var rule_1 = rule_0.Disable(); + + Assert.Throws(() => GuardRule.CanDisable(command, rule_1)); + } + + [Fact] + public void CanDisable_should_not_throw_exception_if_rule_enabled() + { + var command = new DisableRule(); + + var rule_1 = rule_0.Enable(); + + GuardRule.CanDisable(command, rule_1); + } + + [Fact] + public void CanDelete_should_not_throw_exception() + { + var command = new DeleteRule(); + + GuardRule.CanDelete(command); + } + + private CreateRule CreateCommand(CreateRule command) + { + command.AppId = appId; + + return command; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs new file mode 100644 index 000000000..bd76b46bb --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers +{ + public class ContentChangedTriggerTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + + [Fact] + public async Task Should_add_error_if_schema_id_is_not_defined() + { + var trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2()) + }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Schema id is required.", "Schemas") + }); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_add_error_if_schemas_ids_are_not_valid() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + .Returns(Task.FromResult(null)); + + var trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId.Id }) + }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError($"Schema {schemaId.Id} does not exist.", "Schemas") + }); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_is_null() + { + var trigger = new ContentChangedTriggerV2(); + + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_is_empty() + { + var trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Empty() + }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_ids_are_valid() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) + .Returns(Mocks.Schema(appId, schemaId)); + + var trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId.Id }) + }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + + Assert.Empty(errors); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs new file mode 100644 index 000000000..2fcc13a29 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public class ManualTriggerHandlerTests + { + private readonly IRuleTriggerHandler sut = new ManualTriggerHandler(); + + [Fact] + public async Task Should_create_event_with_name() + { + var envelope = Envelope.Create(new RuleManuallyTriggered()); + + var result = await sut.CreateEnrichedEventAsync(envelope); + + Assert.Equal("Manual", result!.Name); + } + + [Fact] + public void Should_always_trigger() + { + Assert.True(sut.Trigger(new EnrichedManualEvent(), new ManualTrigger())); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs new file mode 100644 index 000000000..9eef2daa6 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public class RuleEnqueuerTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + private readonly IRuleEventRepository ruleEventRepository = A.Fake(); + private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly RuleService ruleService = A.Fake(); + private readonly RuleEnqueuer sut; + + public sealed class TestAction : RuleAction + { + public Uri Url { get; set; } + } + + public RuleEnqueuerTests() + { + sut = new RuleEnqueuer( + appProvider, + cache, + ruleEventRepository, + ruleService); + } + + [Fact] + public void Should_return_contents_filter_for_events_filter() + { + Assert.Equal(".*", sut.EventsFilter); + } + + [Fact] + public void Should_return_type_name_for_name() + { + Assert.Equal(typeof(RuleEnqueuer).Name, sut.Name); + } + + [Fact] + public async Task Should_do_nothing_on_clear() + { + await sut.ClearAsync(); + } + + [Fact] + public async Task Should_update_repository_when_enqueing() + { + var @event = Envelope.Create(new ContentCreated { AppId = appId }); + + var rule = CreateRule(); + + var job = new RuleJob { Created = now }; + + A.CallTo(() => ruleService.CreateJobAsync(rule.RuleDef, rule.Id, @event)) + .Returns(job); + + await sut.Enqueue(rule.RuleDef, rule.Id, @event); + + A.CallTo(() => ruleEventRepository.EnqueueAsync(job, now)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_update_repositories_with_jobs_from_service() + { + var @event = Envelope.Create(new ContentCreated { AppId = appId }); + + var rule1 = CreateRule(); + var rule2 = CreateRule(); + + var job1 = new RuleJob { Created = now }; + + A.CallTo(() => appProvider.GetRulesAsync(appId.Id)) + .Returns(new List { rule1, rule2 }); + + A.CallTo(() => ruleService.CreateJobAsync(rule1.RuleDef, rule1.Id, @event)) + .Returns(job1); + + A.CallTo(() => ruleService.CreateJobAsync(rule2.RuleDef, rule2.Id, @event)) + .Returns(Task.FromResult(null)); + + await sut.On(@event); + + A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, now)) + .MustHaveHappened(); + } + + private static RuleEntity CreateRule() + { + var rule = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") }); + + return new RuleEntity { RuleDef = rule, Id = Guid.NewGuid() }; + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs new file mode 100644 index 000000000..7be5758ec --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + public class UsageTriggerHandlerTests + { + private readonly Guid ruleId = Guid.NewGuid(); + private readonly IRuleTriggerHandler sut = new UsageTriggerHandler(); + + [Fact] + public void Should_not_trigger_precheck_when_event_type_not_correct() + { + var result = sut.Trigger(new ContentCreated(), new UsageTrigger(), ruleId); + + Assert.False(result); + } + + [Fact] + public void Should_not_trigger_precheck_when_rule_id_not_matchs() + { + var result = sut.Trigger(new AppUsageExceeded { RuleId = Guid.NewGuid() }, new UsageTrigger(), ruleId); + + Assert.True(result); + } + + [Fact] + public void Should_trigger_precheck_when_event_type_correct_and_rule_id_matchs() + { + var result = sut.Trigger(new AppUsageExceeded { RuleId = ruleId }, new UsageTrigger(), ruleId); + + Assert.True(result); + } + + [Fact] + public void Should_not_trigger_check_when_event_type_not_correct() + { + var result = sut.Trigger(new EnrichedContentEvent(), new UsageTrigger()); + + Assert.False(result); + } + + [Fact] + public async Task Should_create_enriched_event() + { + var @event = new AppUsageExceeded { CallsCurrent = 80, CallsLimit = 120 }; + + var result = await sut.CreateEnrichedEventAsync(Envelope.Create(@event)) as EnrichedUsageExceededEvent; + + Assert.Equal(@event.CallsCurrent, result!.CallsCurrent); + Assert.Equal(@event.CallsLimit, result!.CallsLimit); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ArrayFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ArrayFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ArrayFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ArrayFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/UIFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/UIFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/UIFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/UIFieldPropertiesTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs new file mode 100644 index 000000000..73c01dddc --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs @@ -0,0 +1,379 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards +{ + public class GuardSchemaFieldTests + { + private readonly Schema schema_0; + private readonly StringFieldProperties validProperties = new StringFieldProperties(); + private readonly StringFieldProperties invalidProperties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }; + + public GuardSchemaFieldTests() + { + schema_0 = + new Schema("my-schema") + .AddString(1, "field1", Partitioning.Invariant) + .AddString(2, "field2", Partitioning.Invariant) + .AddArray(3, "field3", Partitioning.Invariant, f => f + .AddNumber(301, "field301")) + .AddUI(4, "field4", Partitioning.Invariant); + } + + private static Action A(Action method) where T : FieldCommand + { + return method; + } + + private static Func S(Func method) + { + return method; + } + + public static IEnumerable FieldCommandData = new[] + { + new object[] { A(GuardSchemaField.CanEnable) }, + new object[] { A(GuardSchemaField.CanDelete) }, + new object[] { A(GuardSchemaField.CanDisable) }, + new object[] { A(GuardSchemaField.CanHide) }, + new object[] { A(GuardSchemaField.CanLock) }, + new object[] { A(GuardSchemaField.CanShow) }, + new object[] { A(GuardSchemaField.CanUpdate) } + }; + + public static IEnumerable InvalidStates = new[] + { + new object[] { A(GuardSchemaField.CanDisable), S(s => s.DisableField(1)) }, + new object[] { A(GuardSchemaField.CanEnable), S(s => s) }, + new object[] { A(GuardSchemaField.CanHide), S(s => s.HideField(1)) }, + new object[] { A(GuardSchemaField.CanShow), S(s => s.LockField(1)) }, + new object[] { A(GuardSchemaField.CanLock), S(s => s.LockField(1)) } + }; + + public static IEnumerable InvalidNestedStates = new[] + { + new object[] { A(GuardSchemaField.CanDisable), S(s => s.DisableField(301, 3)) }, + new object[] { A(GuardSchemaField.CanEnable), S(s => s) }, + new object[] { A(GuardSchemaField.CanHide), S(s => s.HideField(301, 3)) }, + new object[] { A(GuardSchemaField.CanShow), S(s => s) }, + new object[] { A(GuardSchemaField.CanLock), S(s => s.LockField(301, 3)) } + }; + + public static IEnumerable ValidStates = new[] + { + new object[] { A(GuardSchemaField.CanDisable), S(s => s) }, + new object[] { A(GuardSchemaField.CanEnable), S(s => s.DisableField(1)) }, + new object[] { A(GuardSchemaField.CanHide), S(s => s) }, + new object[] { A(GuardSchemaField.CanShow), S(s => s.HideField(1)) } + }; + + public static IEnumerable ValidNestedStates = new[] + { + new object[] { A(GuardSchemaField.CanEnable), S(s => s.DisableField(301, 3)) }, + new object[] { A(GuardSchemaField.CanDisable), S(s => s) }, + new object[] { A(GuardSchemaField.CanHide), S(s => s) }, + new object[] { A(GuardSchemaField.CanShow), S(s => s.HideField(301, 3)) } + }; + + [Theory] + [MemberData(nameof(FieldCommandData))] + public void Commands_should_throw_exception_if_field_not_found(Action action) where T : FieldCommand, new() + { + var command = new T { FieldId = 5 }; + + Assert.Throws(() => action(schema_0, command)); + } + + [Theory] + [MemberData(nameof(FieldCommandData))] + public void Commands_should_throw_exception_if_parent_field_not_found(Action action) where T : FieldCommand, new() + { + var command = new T { ParentFieldId = 4, FieldId = 401 }; + + Assert.Throws(() => action(schema_0, command)); + } + + [Theory] + [MemberData(nameof(FieldCommandData))] + public void Commands_should_throw_exception_if_child_field_not_found(Action action) where T : FieldCommand, new() + { + var command = new T { ParentFieldId = 3, FieldId = 302 }; + + Assert.Throws(() => action(schema_0, command)); + } + + [Theory] + [MemberData(nameof(InvalidStates))] + public void Commands_should_throw_exception_if_state_not_valid(Action action, Func updater) where T : FieldCommand, new() + { + var command = new T { FieldId = 1 }; + + Assert.Throws(() => action(updater(schema_0), command)); + } + + [Theory] + [MemberData(nameof(InvalidNestedStates))] + public void Commands_should_throw_exception_if_nested_state_not_valid(Action action, Func updater) where T : FieldCommand, new() + { + var command = new T { ParentFieldId = 3, FieldId = 301 }; + + Assert.Throws(() => action(updater(schema_0), command)); + } + + [Theory] + [MemberData(nameof(ValidStates))] + public void Commands_should_not_throw_exception_if_state_valid(Action action, Func updater) where T : FieldCommand, new() + { + var command = new T { FieldId = 1 }; + + action(updater(schema_0), command); + } + + [Theory] + [MemberData(nameof(ValidNestedStates))] + public void Commands_should_not_throw_exception_if_nested_state_valid(Action action, Func updater) where T : FieldCommand, new() + { + var command = new T { ParentFieldId = 3, FieldId = 301 }; + + action(updater(schema_0), command); + } + + [Fact] + public void CanDelete_should_throw_exception_if_locked() + { + var command = new DeleteField { FieldId = 1 }; + + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + + Assert.Throws(() => GuardSchemaField.CanDelete(schema_1, command)); + } + + [Fact] + public void CanDisable_should_throw_exception_if_already_disabled() + { + var command = new DisableField { FieldId = 1 }; + + var schema_1 = schema_0.UpdateField(1, f => f.Disable()); + + Assert.Throws(() => GuardSchemaField.CanDisable(schema_1, command)); + } + + [Fact] + public void CanDisable_should_throw_exception_if_ui_field() + { + var command = new DisableField { FieldId = 4 }; + + Assert.Throws(() => GuardSchemaField.CanDisable(schema_0, command)); + } + + [Fact] + public void CanEnable_should_throw_exception_if_already_enabled() + { + var command = new EnableField { FieldId = 1 }; + + Assert.Throws(() => GuardSchemaField.CanEnable(schema_0, command)); + } + + [Fact] + public void CanHide_should_throw_exception_if_locked() + { + var command = new HideField { FieldId = 1 }; + + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + + Assert.Throws(() => GuardSchemaField.CanHide(schema_1, command)); + } + + [Fact] + public void CanHide_should_throw_exception_if_already_hidden() + { + var command = new HideField { FieldId = 1 }; + + var schema_1 = schema_0.UpdateField(1, f => f.Hide()); + + Assert.Throws(() => GuardSchemaField.CanHide(schema_1, command)); + } + + [Fact] + public void CanHide_should_throw_exception_if_ui_field() + { + var command = new HideField { FieldId = 4 }; + + Assert.Throws(() => GuardSchemaField.CanHide(schema_0, command)); + } + + [Fact] + public void CanShow_should_throw_exception_if_already_visible() + { + var command = new ShowField { FieldId = 4 }; + + Assert.Throws(() => GuardSchemaField.CanShow(schema_0, command)); + } + + [Fact] + public void CanDelete_should_not_throw_exception_if_not_locked() + { + var command = new DeleteField { FieldId = 1 }; + + GuardSchemaField.CanDelete(schema_0, command); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_locked() + { + var command = new UpdateField { FieldId = 1, Properties = validProperties }; + + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + + Assert.Throws(() => GuardSchemaField.CanUpdate(schema_1, command)); + } + + [Fact] + public void CanUpdate_should_not_throw_exception_if_not_locked() + { + var command = new UpdateField { FieldId = 1, Properties = validProperties }; + + GuardSchemaField.CanUpdate(schema_0, command); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_marking_a_ui_field_as_list_field() + { + var command = new UpdateField { FieldId = 4, Properties = new UIFieldProperties { IsListField = true } }; + + ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), + new ValidationError("UI field cannot be a list field.", "Properties.IsListField")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_marking_a_ui_field_as_reference_field() + { + var command = new UpdateField { FieldId = 4, Properties = new UIFieldProperties { IsReferenceField = true } }; + + ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), + new ValidationError("UI field cannot be a reference field.", "Properties.IsReferenceField")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_properties_null() + { + var command = new UpdateField { FieldId = 2, Properties = null! }; + + ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), + new ValidationError("Properties is required.", "Properties")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_properties_not_valid() + { + var command = new UpdateField { FieldId = 2, Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } }; + + ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), + new ValidationError("Max length must be greater or equal to min length.", "Properties.MinLength", "Properties.MaxLength")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_field_already_exists() + { + var command = new AddField { Name = "field1", Properties = validProperties }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + new ValidationError("A field with the same name already exists.")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_nested_field_already_exists() + { + var command = new AddField { Name = "field301", Properties = validProperties, ParentFieldId = 3 }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + new ValidationError("A field with the same name already exists.")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_name_not_valid() + { + var command = new AddField { Name = "INVALID_NAME", Properties = validProperties }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + new ValidationError("Name must be a valid javascript property name.", "Name")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_properties_not_valid() + { + var command = new AddField { Name = "field5", Properties = invalidProperties }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + new ValidationError("Max length must be greater or equal to min length.", "Properties.MinLength", "Properties.MaxLength")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_properties_null() + { + var command = new AddField { Name = "field5", Properties = null! }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + new ValidationError("Properties is required.", "Properties")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_partitioning_not_valid() + { + var command = new AddField { Name = "field5", Partitioning = "INVALID_PARTITIONING", Properties = validProperties }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + new ValidationError("Partitioning is not a valid value.", "Partitioning")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_creating_a_ui_field_as_list_field() + { + var command = new AddField { Name = "field5", Properties = new UIFieldProperties { IsListField = true } }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + new ValidationError("UI field cannot be a list field.", "Properties.IsListField")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_parent_not_exists() + { + var command = new AddField { Name = "field302", Properties = validProperties, ParentFieldId = 99 }; + + Assert.Throws(() => GuardSchemaField.CanAdd(schema_0, command)); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_field_not_exists() + { + var command = new AddField { Name = "field5", Properties = validProperties }; + + GuardSchemaField.CanAdd(schema_0, command); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_field_exists_in_root() + { + var command = new AddField { Name = "field1", Properties = validProperties, ParentFieldId = 3 }; + + GuardSchemaField.CanAdd(schema_0, command); + } + } +} \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs new file mode 100644 index 000000000..3e7deb2d8 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs @@ -0,0 +1,530 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards +{ + public class GuardSchemaTests + { + private readonly Schema schema_0; + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + + public GuardSchemaTests() + { + schema_0 = + new Schema("my-schema") + .AddString(1, "field1", Partitioning.Invariant) + .AddString(2, "field2", Partitioning.Invariant); + } + + [Fact] + public void CanCreate_should_throw_exception_if_name_not_valid() + { + var command = new CreateSchema { AppId = appId, Name = "INVALID NAME" }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Name is not a valid slug.", "Name")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_field_name_invalid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "invalid name", + Properties = new StringFieldProperties(), + Partitioning = Partitioning.Invariant.Key + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Field name must be a valid javascript property name.", + "Fields[1].Name")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_field_properties_null() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "field1", + Properties = null!, + Partitioning = Partitioning.Invariant.Key + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Field properties is required.", + "Fields[1].Properties")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_field_properties_not_valid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "field1", + Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }, + Partitioning = Partitioning.Invariant.Key + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Max length must be greater or equal to min length.", + "Fields[1].Properties.MinLength", + "Fields[1].Properties.MaxLength")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_field_partitioning_not_valid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "field1", + Properties = new StringFieldProperties(), + Partitioning = "INVALID" + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Partitioning is not a valid value.", + "Fields[1].Partitioning")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_fields_contains_duplicate_name() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "field1", + Properties = new StringFieldProperties(), + Partitioning = Partitioning.Invariant.Key + }, + new UpsertSchemaField + { + Name = "field1", + Properties = new StringFieldProperties(), + Partitioning = Partitioning.Invariant.Key + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Fields cannot have duplicate names.", + "Fields")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_name_invalid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new List + { + new UpsertSchemaNestedField + { + Name = "invalid name", + Properties = new StringFieldProperties() + } + } + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Field name must be a valid javascript property name.", + "Fields[1].Nested[1].Name")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_properties_null() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new List + { + new UpsertSchemaNestedField + { + Name = "nested1", + Properties = null! + } + } + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Field properties is required.", + "Fields[1].Nested[1].Properties")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_is_array() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new List + { + new UpsertSchemaNestedField + { + Name = "nested1", + Properties = new ArrayFieldProperties() + } + } + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Nested field cannot be array fields.", + "Fields[1].Nested[1].Properties")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_properties_not_valid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new List + { + new UpsertSchemaNestedField + { + Name = "nested1", + Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } + } + } + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Max length must be greater or equal to min length.", + "Fields[1].Nested[1].Properties.MinLength", + "Fields[1].Nested[1].Properties.MaxLength")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_have_duplicate_names() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new List + { + new UpsertSchemaNestedField + { + Name = "nested1", + Properties = new StringFieldProperties() + }, + new UpsertSchemaNestedField + { + Name = "nested1", + Properties = new StringFieldProperties() + } + } + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Fields cannot have duplicate names.", + "Fields[1].Nested")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_ui_field_is_invalid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "field1", + Properties = new UIFieldProperties + { + IsListField = true, + IsReferenceField = true + }, + IsHidden = true, + IsDisabled = true, + Partitioning = Partitioning.Invariant.Key + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("UI field cannot be a list field.", + "Fields[1].Properties.IsListField"), + new ValidationError("UI field cannot be a reference field.", + "Fields[1].Properties.IsReferenceField"), + new ValidationError("UI field cannot be hidden.", + "Fields[1].IsHidden"), + new ValidationError("UI field cannot be disabled.", + "Fields[1].IsDisabled")); + } + + [Fact] + public void CanCreate_should_not_throw_exception_if_command_is_valid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "field1", + Properties = new StringFieldProperties + { + IsListField = true + }, + IsHidden = true, + IsDisabled = true, + Partitioning = Partitioning.Invariant.Key + }, + new UpsertSchemaField + { + Name = "field2", + Properties = ValidProperties(), + Partitioning = Partitioning.Invariant.Key + }, + new UpsertSchemaField + { + Name = "field3", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new List + { + new UpsertSchemaNestedField + { + Name = "nested1", + Properties = ValidProperties() + }, + new UpsertSchemaNestedField + { + Name = "nested2", + Properties = ValidProperties() + } + } + } + }, + Name = "new-schema" + }; + + GuardSchema.CanCreate(command); + } + + [Fact] + public void CanPublish_should_throw_exception_if_already_published() + { + var command = new PublishSchema(); + + var schema_1 = schema_0.Publish(); + + Assert.Throws(() => GuardSchema.CanPublish(schema_1, command)); + } + + [Fact] + public void CanPublish_should_not_throw_exception_if_not_published() + { + var command = new PublishSchema(); + + GuardSchema.CanPublish(schema_0, command); + } + + [Fact] + public void CanUnpublish_should_throw_exception_if_already_unpublished() + { + var command = new UnpublishSchema(); + + Assert.Throws(() => GuardSchema.CanUnpublish(schema_0, command)); + } + + [Fact] + public void CanUnpublish_should_not_throw_exception_if_already_published() + { + var command = new UnpublishSchema(); + + var schema_1 = schema_0.Publish(); + + GuardSchema.CanUnpublish(schema_1, command); + } + + [Fact] + public void CanReorder_should_throw_exception_if_field_ids_contains_invalid_id() + { + var command = new ReorderFields { FieldIds = new List { 1, 3 } }; + + ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), + new ValidationError("Field ids do not cover all fields.", "FieldIds")); + } + + [Fact] + public void CanReorder_should_throw_exception_if_field_ids_do_not_covers_all_fields() + { + var command = new ReorderFields { FieldIds = new List { 1 } }; + + ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), + new ValidationError("Field ids do not cover all fields.", "FieldIds")); + } + + [Fact] + public void CanReorder_should_throw_exception_if_field_ids_null() + { + var command = new ReorderFields { FieldIds = null! }; + + ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), + new ValidationError("Field ids is required.", "FieldIds")); + } + + [Fact] + public void CanReorder_should_throw_exception_if_parent_field_not_found() + { + var command = new ReorderFields { FieldIds = new List { 1, 2 }, ParentFieldId = 99 }; + + Assert.Throws(() => GuardSchema.CanReorder(schema_0, command)); + } + + [Fact] + public void CanReorder_should_not_throw_exception_if_field_ids_are_valid() + { + var command = new ReorderFields { FieldIds = new List { 1, 2 } }; + + GuardSchema.CanReorder(schema_0, command); + } + + [Fact] + public void CanConfigurePreviewUrls_should_throw_exception_if_preview_urls_null() + { + var command = new ConfigurePreviewUrls { PreviewUrls = null! }; + + ValidationAssert.Throws(() => GuardSchema.CanConfigurePreviewUrls(command), + new ValidationError("Preview Urls is required.", "PreviewUrls")); + } + + [Fact] + public void CanConfigurePreviewUrls_should_not_throw_exception_if_valid() + { + var command = new ConfigurePreviewUrls { PreviewUrls = new Dictionary() }; + + GuardSchema.CanConfigurePreviewUrls(command); + } + + [Fact] + public void CanChangeCategory_should_not_throw_exception() + { + var command = new ChangeCategory(); + + GuardSchema.CanChangeCategory(schema_0, command); + } + + [Fact] + public void CanDelete_should_not_throw_exception() + { + var command = new DeleteSchema(); + + GuardSchema.CanDelete(schema_0, command); + } + + private static StringFieldProperties ValidProperties() + { + return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs new file mode 100644 index 000000000..5befc397b --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs @@ -0,0 +1,248 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public class SchemasIndexTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly ISchemasByAppIndexGrain index = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly SchemasIndex sut; + + public SchemasIndexTests() + { + A.CallTo(() => grainFactory.GetGrain(appId.Id, null)) + .Returns(index); + + sut = new SchemasIndex(grainFactory); + } + + [Fact] + public async Task Should_resolve_schema_by_id() + { + var schema = SetupSchema(0, false); + + var actual = await sut.GetSchemaAsync(appId.Id, schema.Id); + + Assert.Same(actual, schema); + } + + [Fact] + public async Task Should_resolve_schema_by_name() + { + var schema = SetupSchema(0, false); + + A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) + .Returns(schema.Id); + + var actual = await sut.GetSchemaByNameAsync(appId.Id, schema.SchemaDef.Name); + + Assert.Same(actual, schema); + } + + [Fact] + public async Task Should_resolve_schemas_by_id() + { + var schema = SetupSchema(0, false); + + A.CallTo(() => index.GetIdsAsync()) + .Returns(new List { schema.Id }); + + var actual = await sut.GetSchemasAsync(appId.Id); + + Assert.Same(actual[0], schema); + } + + [Fact] + public async Task Should_return_empty_schema_if_schema_not_created() + { + var schema = SetupSchema(-1, false); + + A.CallTo(() => index.GetIdsAsync()) + .Returns(new List { schema.Id }); + + var actual = await sut.GetSchemasAsync(appId.Id); + + Assert.Empty(actual); + } + + [Fact] + public async Task Should_return_empty_schema_if_schema_deleted() + { + var schema = SetupSchema(0, true); + + A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) + .Returns(schema.Id); + + var actual = await sut.GetSchemasAsync(appId.Id); + + Assert.Empty(actual); + } + + [Fact] + public async Task Should_also_return_schema_if_deleted_allowed() + { + var schema = SetupSchema(-1, true); + + A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) + .Returns(schema.Id); + + var actual = await sut.GetSchemasAsync(appId.Id, true); + + Assert.Empty(actual); + } + + [Fact] + public async Task Should_add_schema_to_index_on_create() + { + var token = RandomHash.Simple(); + + A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) + .Returns(token); + + var context = + new CommandContext(Create(schemaId.Name), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.AddAsync(token)) + .MustHaveHappened(); + + A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_clear_reservation_when_schema_creation_failed() + { + var token = RandomHash.Simple(); + + A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) + .Returns(token); + + var context = + new CommandContext(Create(schemaId.Name), commandBus); + + await sut.HandleAsync(context); + + A.CallTo(() => index.AddAsync(token)) + .MustNotHaveHappened(); + + A.CallTo(() => index.RemoveReservationAsync(token)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_add_to_index_on_create_if_name_taken() + { + A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) + .Returns(Task.FromResult(null)); + + var context = + new CommandContext(Create(schemaId.Name), commandBus) + .Complete(); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + + A.CallTo(() => index.AddAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_add_to_index_on_create_if_name_invalid() + { + var context = + new CommandContext(Create("INVALID"), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.ReserveAsync(schemaId.Id, A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_remove_schema_from_index_on_delete() + { + var schema = SetupSchema(0, false); + + var context = + new CommandContext(new DeleteSchema { SchemaId = schema.Id }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.RemoveAsync(schema.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_call_when_rebuilding() + { + var schemas = new Dictionary(); + + await sut.RebuildAsync(appId.Id, schemas); + + A.CallTo(() => index.RebuildAsync(schemas)) + .MustHaveHappened(); + } + + private CreateSchema Create(string name) + { + return new CreateSchema { SchemaId = schemaId.Id, Name = name, AppId = appId }; + } + + private ISchemaEntity SetupSchema(long version, bool deleted) + { + var schemaEntity = A.Fake(); + + A.CallTo(() => schemaEntity.SchemaDef) + .Returns(new Schema(schemaId.Name)); + A.CallTo(() => schemaEntity.Id) + .Returns(schemaId.Id); + A.CallTo(() => schemaEntity.AppId) + .Returns(appId); + A.CallTo(() => schemaEntity.Version) + .Returns(version); + A.CallTo(() => schemaEntity.IsDeleted) + .Returns(deleted); + + var schemaGrain = A.Fake(); + + A.CallTo(() => schemaGrain.GetStateAsync()) + .Returns(J.Of(schemaEntity)); + + A.CallTo(() => grainFactory.GetGrain(schemaId.Id, null)) + .Returns(schemaGrain); + + return schemaEntity; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs new file mode 100644 index 000000000..dbbd928b5 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs @@ -0,0 +1,146 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public class SchemaChangedTriggerHandlerTests + { + private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IRuleTriggerHandler sut; + + public SchemaChangedTriggerHandlerTests() + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) + .Returns(true); + + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) + .Returns(false); + + sut = new SchemaChangedTriggerHandler(scriptEngine); + } + + public static IEnumerable TestEvents = new[] + { + new object[] { new SchemaCreated(), EnrichedSchemaEventType.Created }, + new object[] { new SchemaUpdated(), EnrichedSchemaEventType.Updated }, + new object[] { new SchemaDeleted(), EnrichedSchemaEventType.Deleted }, + new object[] { new SchemaPublished(), EnrichedSchemaEventType.Published }, + new object[] { new SchemaUnpublished(), EnrichedSchemaEventType.Unpublished } + }; + + [Theory] + [MemberData(nameof(TestEvents))] + public async Task Should_enrich_events(SchemaEvent @event, EnrichedSchemaEventType type) + { + var envelope = Envelope.Create(@event).SetEventStreamNumber(12); + + var result = await sut.CreateEnrichedEventAsync(envelope); + + Assert.Equal(type, ((EnrichedSchemaEvent)result!).Type); + } + + [Fact] + public void Should_not_trigger_precheck_when_event_type_not_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new AppCreated(), trigger, Guid.NewGuid()); + + Assert.False(result); + }); + } + + [Fact] + public void Should_trigger_precheck_when_event_type_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new SchemaCreated(), trigger, Guid.NewGuid()); + + Assert.True(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_event_type_not_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new EnrichedContentEvent(), trigger); + + Assert.False(result); + }); + } + + [Fact] + public void Should_trigger_check_when_condition_is_empty() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_trigger_check_when_condition_matchs() + { + TestForCondition("true", trigger => + { + var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_condition_does_not_matchs() + { + TestForCondition("false", trigger => + { + var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); + + Assert.False(result); + }); + } + + private void TestForCondition(string condition, Action action) + { + var trigger = new SchemaChangedTrigger { Condition = condition }; + + action(trigger); + + if (string.IsNullOrWhiteSpace(condition)) + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) + .MustNotHaveHappened(); + } + else + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) + .MustHaveHappened(); + } + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj new file mode 100644 index 000000000..81e4d6ec5 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -0,0 +1,40 @@ + + + Exe + netcoreapp3.0 + Squidex.Domain.Apps.Entities + 8.0 + enable + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs new file mode 100644 index 000000000..1fbb559f4 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.TestHelpers +{ + public static class AExtensions + { + public static ClrQuery Is(this INegatableArgumentConstraintManager that, string query) + { + return that.Matches(x => x.ToString() == query); + } + + public static T[] Is(this INegatableArgumentConstraintManager that, params T[]? values) + { + if (values == null) + { + return that.IsNull(); + } + + return that.IsSameSequenceAs(values); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/JsonHelper.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/JsonHelper.cs new file mode 100644 index 000000000..01871cca9 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/JsonHelper.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Newtonsoft; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.TestHelpers +{ + public static class JsonHelper + { + public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); + + public static IJsonSerializer CreateSerializer(TypeNameRegistry? typeNameRegistry = null) + { + var serializerSettings = DefaultSettings(typeNameRegistry); + + return new NewtonsoftJsonSerializer(serializerSettings); + } + + public static JsonSerializerSettings DefaultSettings(TypeNameRegistry? typeNameRegistry = null) + { + return new JsonSerializerSettings + { + SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry ?? new TypeNameRegistry()), + + ContractResolver = new ConverterContractResolver( + new ClaimsPrincipalConverter(), + new InstantConverter(), + new EnvelopeHeadersConverter(), + new FilterConverter(), + new JsonValueConverter(), + new LanguageConverter(), + new NamedGuidIdConverter(), + new NamedLongIdConverter(), + new NamedStringIdConverter(), + new PropertyPathConverter(), + new RefTokenConverter(), + new StringEnumConverter()), + + TypeNameHandling = TypeNameHandling.Auto + }; + } + + public static T SerializeAndDeserialize(this T value) + { + return DefaultSerializer.Deserialize>(DefaultSerializer.Serialize(Tuple.Create(value))).Item1; + } + + public static T Deserialize(string value) + { + return DefaultSerializer.Deserialize>($"{{ \"Item1\": \"{value}\" }}").Item1; + } + + public static T Deserialize(object value) + { + return DefaultSerializer.Deserialize>($"{{ \"Item1\": {value} }}").Item1; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs new file mode 100644 index 000000000..ad363bb6b --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Security.Claims; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Squidex.Shared; + +namespace Squidex.Domain.Apps.Entities.TestHelpers +{ + public static class Mocks + { + public static IAppEntity App(NamedId appId, params Language[] languages) + { + var config = LanguagesConfig.English; + + foreach (var language in languages) + { + config = config.Set(language); + } + + var app = A.Fake(); + + A.CallTo(() => app.Id).Returns(appId.Id); + A.CallTo(() => app.Name).Returns(appId.Name); + A.CallTo(() => app.LanguagesConfig).Returns(config); + + return app; + } + + public static ISchemaEntity Schema(NamedId appId, NamedId schemaId, Schema? schemaDef = null) + { + var schema = A.Fake(); + + A.CallTo(() => schema.Id).Returns(schemaId.Id); + A.CallTo(() => schema.AppId).Returns(appId); + A.CallTo(() => schema.SchemaDef).Returns(schemaDef ?? new Schema(schemaId.Name)); + + return schema; + } + + public static ClaimsPrincipal ApiUser(string? role = null) + { + return CreateUser(role, "api"); + } + + public static ClaimsPrincipal FrontendUser(string? role = null) + { + return CreateUser(role, DefaultClients.Frontend); + } + + private static ClaimsPrincipal CreateUser(string? role, string client) + { + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + + claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, client)); + + if (role != null) + { + claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role)); + } + + return claimsPrincipal; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/ValidationAssert.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/ValidationAssert.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/ValidationAssert.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/ValidationAssert.cs diff --git a/tests/Squidex.Domain.Users.Tests/AssetUserPictureStoreTests.cs b/backend/tests/Squidex.Domain.Users.Tests/AssetUserPictureStoreTests.cs similarity index 100% rename from tests/Squidex.Domain.Users.Tests/AssetUserPictureStoreTests.cs rename to backend/tests/Squidex.Domain.Users.Tests/AssetUserPictureStoreTests.cs diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs new file mode 100644 index 000000000..ecb332377 --- /dev/null +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs @@ -0,0 +1,135 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Squidex.Domain.Users +{ + public class DefaultUserResolverTests + { + private readonly UserManager userManager = A.Fake>(); + private readonly DefaultUserResolver sut; + + public DefaultUserResolverTests() + { + var userFactory = A.Fake(); + + A.CallTo(() => userFactory.IsId(A.That.StartsWith("id"))) + .Returns(true); + + A.CallTo(() => userManager.NormalizeEmail(A.Ignored)) + .ReturnsLazily(c => c.GetArgument(0).ToUpperInvariant()); + + var serviceProvider = A.Fake(); + + var scope = A.Fake(); + + var scopeFactory = A.Fake(); + + A.CallTo(() => scopeFactory.CreateScope()) + .Returns(scope); + + A.CallTo(() => scope.ServiceProvider) + .Returns(serviceProvider); + + A.CallTo(() => serviceProvider.GetService(typeof(IServiceScopeFactory))) + .Returns(scopeFactory); + + A.CallTo(() => serviceProvider.GetService(typeof(IUserFactory))) + .Returns(userFactory); + + A.CallTo(() => serviceProvider.GetService(typeof(UserManager))) + .Returns(userManager); + + sut = new DefaultUserResolver(serviceProvider); + } + + [Fact] + public async Task Should_resolve_user_by_email() + { + var (user, claims) = GernerateUser("id1"); + + A.CallTo(() => userManager.FindByEmailAsync(user.Email)) + .Returns(user); + + A.CallTo(() => userManager.GetClaimsAsync(user)) + .Returns(claims); + + var result = await sut.FindByIdOrEmailAsync(user.Email); + + Assert.Equal(user.Id, result!.Id); + Assert.Equal(user.Email, result!.Email); + + Assert.Equal(claims, result!.Claims); + } + + [Fact] + public async Task Should_resolve_user_by_id1() + { + var (user, claims) = GernerateUser("id2"); + + A.CallTo(() => userManager.FindByIdAsync(user.Id)) + .Returns(user); + + A.CallTo(() => userManager.GetClaimsAsync(user)) + .Returns(claims); + + var result = await sut.FindByIdOrEmailAsync(user.Id)!; + + Assert.Equal(user.Id, result!.Id); + Assert.Equal(user.Email, result!.Email); + + Assert.Equal(claims, result!.Claims); + } + + [Fact] + public async Task Should_query_many_by_email_async() + { + var (user1, claims1) = GernerateUser("id1"); + var (user2, claims2) = GernerateUser("id2"); + + var list = new List { user1, user2 }; + + A.CallTo(() => userManager.Users) + .Returns(list.AsQueryable()); + + A.CallTo(() => userManager.GetClaimsAsync(user2)) + .Returns(claims2); + + var result = await sut.QueryByEmailAsync("2"); + + Assert.Equal(user2.Id, result[0].Id); + Assert.Equal(user2.Email, result[0].Email); + + Assert.Equal(claims2, result[0].Claims); + + A.CallTo(() => userManager.GetClaimsAsync(user1)) + .MustNotHaveHappened(); + } + + private static (IdentityUser, List) GernerateUser(string id) + { + var user = new IdentityUser { Id = id, Email = $"email_{id}", NormalizedEmail = $"EMAIL_{id}" }; + + var claims = new List + { + new Claim($"{id}_a", "1"), + new Claim($"{id}_b", "2") + }; + + return (user, claims); + } + } +} diff --git a/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs similarity index 100% rename from tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs rename to backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs diff --git a/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj new file mode 100644 index 000000000..c7ad65860 --- /dev/null +++ b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj @@ -0,0 +1,34 @@ + + + Exe + netcoreapp3.0 + Squidex.Domain.Users + 8.0 + enable + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AssetExtensionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/AssetExtensionTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/AssetExtensionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/AssetExtensionTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs new file mode 100644 index 000000000..2e3548b5a --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs @@ -0,0 +1,164 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.Assets +{ + public abstract class AssetStoreTests where T : IAssetStore + { + private readonly MemoryStream assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 }); + private readonly string fileName = Guid.NewGuid().ToString(); + private readonly string sourceFile = Guid.NewGuid().ToString(); + private readonly Lazy sut; + + protected T Sut + { + get { return sut.Value; } + } + + protected string FileName + { + get { return fileName; } + } + + protected AssetStoreTests() + { + sut = new Lazy(CreateStore); + } + + public abstract T CreateStore(); + + [Fact] + public virtual async Task Should_throw_exception_if_asset_to_download_is_not_found() + { + await Assert.ThrowsAsync(() => Sut.DownloadAsync(fileName, new MemoryStream())); + } + + [Fact] + public async Task Should_throw_exception_if_asset_to_copy_is_not_found() + { + await Assert.ThrowsAsync(() => Sut.CopyAsync(fileName, sourceFile)); + } + + [Fact] + public async Task Should_throw_exception_if_stream_to_download_is_null() + { + await Assert.ThrowsAsync(() => Sut.DownloadAsync("File", null!)); + } + + [Fact] + public async Task Should_throw_exception_if_stream_to_upload_is_null() + { + await Assert.ThrowsAsync(() => Sut.UploadAsync("File", null!)); + } + + [Fact] + public async Task Should_throw_exception_if_source_file_name_to_copy_is_empty() + { + await CheckEmpty(v => Sut.CopyAsync(v, "Target")); + } + + [Fact] + public async Task Should_throw_exception_if_target_file_name_to_copy_is_empty() + { + await CheckEmpty(v => Sut.CopyAsync("Source", v)); + } + + [Fact] + public async Task Should_throw_exception_if_file_name_to_delete_is_empty() + { + await CheckEmpty(v => Sut.DeleteAsync(v)); + } + + [Fact] + public async Task Should_throw_exception_if_file_name_to_download_is_empty() + { + await CheckEmpty(v => Sut.DownloadAsync(v, new MemoryStream())); + } + + [Fact] + public async Task Should_throw_exception_if_file_name_to_upload_is_empty() + { + await CheckEmpty(v => Sut.UploadAsync(v, new MemoryStream())); + } + + [Fact] + public async Task Should_write_and_read_file() + { + await Sut.UploadAsync(fileName, assetData); + + var readData = new MemoryStream(); + + await Sut.DownloadAsync(fileName, readData); + + Assert.Equal(assetData.ToArray(), readData.ToArray()); + } + + [Fact] + public async Task Should_write_and_read_file_and_overwrite_non_existing() + { + await Sut.UploadAsync(fileName, assetData, true); + + var readData = new MemoryStream(); + + await Sut.DownloadAsync(fileName, readData); + + Assert.Equal(assetData.ToArray(), readData.ToArray()); + } + + [Fact] + public async Task Should_write_and_read_overriding_file() + { + var oldData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5 }); + + await Sut.UploadAsync(fileName, oldData); + await Sut.UploadAsync(fileName, assetData, true); + + var readData = new MemoryStream(); + + await Sut.DownloadAsync(fileName, readData); + + Assert.Equal(assetData.ToArray(), readData.ToArray()); + } + + [Fact] + public async Task Should_throw_exception_when_file_to_write_already_exists() + { + await Sut.UploadAsync(fileName, assetData); + + await Assert.ThrowsAsync(() => Sut.UploadAsync(fileName, assetData)); + } + + [Fact] + public async Task Should_throw_exception_when_target_file_to_copy_to_already_exists() + { + await Sut.UploadAsync(sourceFile, assetData); + await Sut.CopyAsync(sourceFile, fileName); + + await Assert.ThrowsAsync(() => Sut.CopyAsync(sourceFile, fileName)); + } + + [Fact] + public async Task Should_ignore_when_deleting_not_existing_file() + { + await Sut.UploadAsync(sourceFile, assetData); + await Sut.DeleteAsync(sourceFile); + await Sut.DeleteAsync(sourceFile); + } + + private static async Task CheckEmpty(Func action) + { + await Assert.ThrowsAsync(() => action(null!)); + await Assert.ThrowsAsync(() => action(string.Empty)); + await Assert.ThrowsAsync(() => action(" ")); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/HasherStreamTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/HasherStreamTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/HasherStreamTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/HasherStreamTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs new file mode 100644 index 000000000..12446e777 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs @@ -0,0 +1,92 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using Squidex.Infrastructure.Assets.ImageSharp; +using Xunit; + +namespace Squidex.Infrastructure.Assets +{ + public class ImageSharpAssetThumbnailGeneratorTests + { + private readonly ImageSharpAssetThumbnailGenerator sut = new ImageSharpAssetThumbnailGenerator(); + private readonly MemoryStream target = new MemoryStream(); + + [Fact] + public async Task Should_return_same_image_if_no_size_is_passed_for_thumbnail() + { + var source = GetPng(); + + await sut.CreateThumbnailAsync(source, target); + + Assert.Equal(target.Length, source.Length); + } + + [Fact] + public async Task Should_resize_image_to_target() + { + var source = GetPng(); + + await sut.CreateThumbnailAsync(source, target, 1000, 1000, "resize"); + + Assert.True(target.Length > source.Length); + } + + [Fact] + public async Task Should_change_jpeg_quality_and_write_to_target() + { + var source = GetJpeg(); + + await sut.CreateThumbnailAsync(source, target, quality: 10); + + Assert.True(target.Length < source.Length); + } + + [Fact] + public async Task Should_change_png_quality_and_write_to_target() + { + var source = GetPng(); + + await sut.CreateThumbnailAsync(source, target, quality: 10); + + Assert.True(target.Length < source.Length); + } + + [Fact] + public async Task Should_return_image_information_if_image_is_valid() + { + var source = GetPng(); + + var imageInfo = await sut.GetImageInfoAsync(source); + + Assert.Equal(600, imageInfo!.PixelHeight); + Assert.Equal(600, imageInfo!.PixelWidth); + } + + [Fact] + public async Task Should_return_null_if_stream_is_not_an_image() + { + var source = new MemoryStream(Convert.FromBase64String("YXNkc2Fk")); + + var imageInfo = await sut.GetImageInfoAsync(source); + + Assert.Null(imageInfo); + } + + private Stream GetPng() + { + return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.png")!; + } + + private Stream GetJpeg() + { + return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.jpg")!; + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg b/backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg rename to backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg diff --git a/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png b/backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png rename to backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png diff --git a/tests/Squidex.Infrastructure.Tests/Assets/MemoryAssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/MemoryAssetStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/MemoryAssetStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/MemoryAssetStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFSAssetStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFSAssetStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/MongoGridFSAssetStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFSAssetStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs new file mode 100644 index 000000000..223bb4ffb --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs @@ -0,0 +1,278 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class CollectionExtensionsTests + { + private readonly Dictionary valueDictionary = new Dictionary(); + private readonly Dictionary> listDictionary = new Dictionary>(); + + [Fact] + public void GetOrDefault_should_return_value_if_key_exists() + { + valueDictionary[12] = 34; + + Assert.Equal(34, valueDictionary.GetOrDefault(12)); + } + + [Fact] + public void GetOrDefault_should_return_default_and_not_add_it_if_key_not_exists() + { + Assert.Equal(0, valueDictionary.GetOrDefault(12)); + Assert.False(valueDictionary.ContainsKey(12)); + } + + [Fact] + public void GetOrAddDefault_should_return_value_if_key_exists() + { + valueDictionary[12] = 34; + + Assert.Equal(34, valueDictionary.GetOrAddDefault(12)); + } + + [Fact] + public void GetOrAddDefault_should_return_default_and_add_it_if_key_not_exists() + { + Assert.Equal(0, valueDictionary.GetOrAddDefault(12)); + Assert.Equal(0, valueDictionary[12]); + } + + [Fact] + public void GetOrCreate_should_return_value_if_key_exists() + { + valueDictionary[12] = 34; + + Assert.Equal(34, valueDictionary.GetOrCreate(12, x => 34)); + } + + [Fact] + public void GetOrCreate_should_return_default_but_not_add_it_if_key_not_exists() + { + Assert.Equal(24, valueDictionary.GetOrCreate(12, x => 24)); + Assert.False(valueDictionary.ContainsKey(12)); + } + + [Fact] + public void GetOrAdd_should_return_value_if_key_exists() + { + valueDictionary[12] = 34; + + Assert.Equal(34, valueDictionary.GetOrAdd(12, x => 34)); + } + + [Fact] + public void GetOrAdd_should_return_default_and_add_it_if_key_not_exists() + { + Assert.Equal(24, valueDictionary.GetOrAdd(12, 24)); + Assert.Equal(24, valueDictionary[12]); + } + + [Fact] + public void GetOrAdd_should_return_default_and_add_it_with_fallback_if_key_not_exists() + { + Assert.Equal(24, valueDictionary.GetOrAdd(12, x => 24)); + Assert.Equal(24, valueDictionary[12]); + } + + [Fact] + public void GetOrNew_should_return_value_if_key_exists() + { + var list = new List(); + listDictionary[12] = list; + + Assert.Equal(list, listDictionary.GetOrNew(12)); + } + + [Fact] + public void GetOrNew_should_return_default_but_not_add_it_if_key_not_exists() + { + var list = new List(); + + Assert.Equal(list, listDictionary.GetOrNew(12)); + Assert.False(listDictionary.ContainsKey(12)); + } + + [Fact] + public void GetOrAddNew_should_return_value_if_key_exists() + { + var list = new List(); + listDictionary[12] = list; + + Assert.Equal(list, listDictionary.GetOrAddNew(12)); + } + + [Fact] + public void GetOrAddNew_should_return_default_but_not_add_it_if_key_not_exists() + { + var list = new List(); + + Assert.Equal(list, listDictionary.GetOrAddNew(12)); + Assert.Equal(list, listDictionary[12]); + } + + [Fact] + public void SequentialHashCode_should_ignore_null_values() + { + var collection = new string?[] { null, null }; + + Assert.Equal(17, collection.SequentialHashCode()); + } + + [Fact] + public void SequentialHashCode_should_return_same_hash_codes_for_list_with_same_order() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 3, 5, 6 }; + + Assert.Equal(collection2.SequentialHashCode(), collection1.SequentialHashCode()); + } + + [Fact] + public void SequentialHashCode_should_return_different_hash_codes_for_list_with_different_items() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 3, 4, 1 }; + + Assert.NotEqual(collection2.SequentialHashCode(), collection1.SequentialHashCode()); + } + + [Fact] + public void SequentialHashCode_should_return_different_hash_codes_for_list_with_different_order() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 6, 5, 3 }; + + Assert.NotEqual(collection2.SequentialHashCode(), collection1.SequentialHashCode()); + } + + [Fact] + public void OrderedHashCode_should_return_same_hash_codes_for_list_with_same_order() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 3, 5, 6 }; + + Assert.Equal(collection2.OrderedHashCode(), collection1.OrderedHashCode()); + } + + [Fact] + public void OrderedHashCode_should_return_different_hash_codes_for_list_with_different_items() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 3, 4, 1 }; + + Assert.NotEqual(collection2.OrderedHashCode(), collection1.OrderedHashCode()); + } + + [Fact] + public void OrderedHashCode_should_return_same_hash_codes_for_list_with_different_order() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 6, 5, 3 }; + + Assert.Equal(collection2.OrderedHashCode(), collection1.OrderedHashCode()); + } + + [Fact] + public void EqualsDictionary_should_return_true_for_equal_dictionaries() + { + var lhs = new Dictionary + { + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary + { + [1] = 1, + [2] = 2 + }; + + Assert.True(lhs.EqualsDictionary(rhs)); + } + + [Fact] + public void EqualsDictionary_should_return_false_for_different_sizes() + { + var lhs = new Dictionary + { + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary + { + [1] = 1 + }; + + Assert.False(lhs.EqualsDictionary(rhs)); + } + + [Fact] + public void EqualsDictionary_should_return_false_for_different_values() + { + var lhs = new Dictionary + { + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary + { + [1] = 1, + [3] = 3 + }; + + Assert.False(lhs.EqualsDictionary(rhs)); + } + + [Fact] + public void Dictionary_should_return_same_hashcode_for_equal_dictionaries() + { + var lhs = new Dictionary + { + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary + { + [1] = 1, + [2] = 2 + }; + + Assert.Equal(lhs.DictionaryHashCode(), rhs.DictionaryHashCode()); + } + + [Fact] + public void Dictionary_should_return_different_hashcode_for_different_dictionaries() + { + var lhs = new Dictionary + { + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary + { + [1] = 1, + [3] = 3 + }; + + Assert.NotEqual(lhs.DictionaryHashCode(), rhs.DictionaryHashCode()); + } + + [Fact] + public void Foreach_should_call_action_foreach_item() + { + var source = new List { 3, 5, 1 }; + var target = new List(); + + source.Foreach(target.Add); + + Assert.Equal(source, target); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs new file mode 100644 index 000000000..ccd843166 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Reflection; +using FakeItEasy; +using Orleans; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Commands +{ + public class DomainObjectGrainFormatterTests + { + private readonly IGrainCallContext context = A.Fake(); + + [Fact] + public void Should_return_fallback_if_no_method_is_defined() + { + A.CallTo(() => context.InterfaceMethod) + .Returns(null!); + + var result = DomainObjectGrainFormatter.Format(context); + + Assert.Equal("Unknown", result); + } + + [Fact] + public void Should_return_method_name_if_not_domain_object_method() + { + var methodInfo = A.Fake(); + + A.CallTo(() => methodInfo.Name) + .Returns("Calculate"); + + A.CallTo(() => context.InterfaceMethod) + .Returns(methodInfo); + + var result = DomainObjectGrainFormatter.Format(context); + + Assert.Equal("Calculate", result); + } + + [Fact] + public void Should_return_nice_method_name_if_domain_object_execute() + { + var methodInfo = A.Fake(); + + A.CallTo(() => methodInfo.Name) + .Returns(nameof(IDomainObjectGrain.ExecuteAsync)); + + A.CallTo(() => context.Arguments) + .Returns(new object[] { new MyCommand() }); + + A.CallTo(() => context.InterfaceMethod) + .Returns(methodInfo); + + var result = DomainObjectGrainFormatter.Format(context); + + Assert.Equal("ExecuteAsync(MyCommand)", result); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs new file mode 100644 index 000000000..6803b7948 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs @@ -0,0 +1,220 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Commands +{ + public class DomainObjectGrainTests + { + private readonly IStore store = A.Fake>(); + private readonly IPersistence persistence = A.Fake>(); + private readonly Guid id = Guid.NewGuid(); + private readonly MyDomainObject sut; + + public sealed class MyDomainObject : DomainObjectGrain + { + public MyDomainObject(IStore store) + : base(store, A.Dummy()) + { + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + switch (command) + { + case CreateAuto createAuto: + return Create(createAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case CreateCustom createCustom: + return CreateReturn(createCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "CREATED"; + }); + + case UpdateAuto updateAuto: + return Update(updateAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case UpdateCustom updateCustom: + return UpdateReturn(updateCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "UPDATED"; + }); + } + + return Task.FromResult(null); + } + } + + public DomainObjectGrainTests() + { + A.CallTo(() => store.WithSnapshotsAndEventSourcing(typeof(MyDomainObject), id, A>.Ignored, A.Ignored)) + .Returns(persistence); + + sut = new MyDomainObject(store); + } + + [Fact] + public void Should_instantiate() + { + Assert.Equal(EtagVersion.Empty, sut.Version); + } + + [Fact] + public async Task Should_write_state_and_events_when_created() + { + await SetupEmptyAsync(); + + var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 4))) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) + .MustHaveHappened(); + + Assert.True(result.Value is EntityCreatedResult); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(4, sut.Snapshot.Value); + Assert.Equal(0, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_write_state_and_events_when_updated() + { + await SetupCreatedAsync(); + + var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 8))) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) + .MustHaveHappened(); + + Assert.True(result.Value is EntitySavedResult); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(8, sut.Snapshot.Value); + Assert.Equal(1, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_throw_exception_when_already_created() + { + await SetupCreatedAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); + } + + [Fact] + public async Task Should_throw_exception_when_not_created() + { + await SetupEmptyAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); + } + + [Fact] + public async Task Should_return_custom_result_on_create() + { + await SetupEmptyAsync(); + + var result = await sut.ExecuteAsync(C(new CreateCustom())); + + Assert.Equal("CREATED", result.Value); + } + + [Fact] + public async Task Should_return_custom_result_on_update() + { + await SetupCreatedAsync(); + + var result = await sut.ExecuteAsync(C(new UpdateCustom())); + + Assert.Equal("UPDATED", result.Value); + } + + [Fact] + public async Task Should_throw_exception_when_other_verison_expected() + { + await SetupCreatedAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 }))); + } + + [Fact] + public async Task Should_reset_state_when_writing_snapshot_for_create_failed() + { + await SetupEmptyAsync(); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(0, sut.Snapshot.Value); + Assert.Equal(-1, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_reset_state_when_writing_snapshot_for_update_failed() + { + await SetupCreatedAsync(); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(4, sut.Snapshot.Value); + Assert.Equal(0, sut.Snapshot.Version); + } + + private async Task SetupCreatedAsync() + { + await sut.ActivateAsync(id); + + await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); + } + + private static J C(IAggregateCommand command) + { + return command.AsJ(); + } + + private async Task SetupEmptyAsync() + { + await sut.ActivateAsync(id); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs new file mode 100644 index 000000000..38d92a242 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs @@ -0,0 +1,280 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Commands +{ + public class LogSnapshotDomainObjectGrainTests + { + private readonly IStore store = A.Fake>(); + private readonly ISnapshotStore snapshotStore = A.Fake>(); + private readonly IPersistence persistence = A.Fake(); + private readonly Guid id = Guid.NewGuid(); + private readonly MyLogDomainObject sut; + + public sealed class MyLogDomainObject : LogSnapshotDomainObjectGrain + { + public MyLogDomainObject(IStore store) + : base(store, A.Dummy()) + { + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + switch (command) + { + case CreateAuto createAuto: + return Create(createAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case CreateCustom createCustom: + return CreateReturn(createCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "CREATED"; + }); + + case UpdateAuto updateAuto: + return Update(updateAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case UpdateCustom updateCustom: + return UpdateReturn(updateCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "UPDATED"; + }); + } + + return Task.FromResult(null); + } + } + + public LogSnapshotDomainObjectGrainTests() + { + A.CallTo(() => store.WithEventSourcing(typeof(MyLogDomainObject), id, A.Ignored)) + .Returns(persistence); + + A.CallTo(() => store.GetSnapshotStore()) + .Returns(snapshotStore); + + sut = new MyLogDomainObject(store); + } + + [Fact] + public async Task Should_get_latestet_version_when_requesting_state_with_any() + { + await SetupUpdatedAsync(); + + var result = sut.GetSnapshot(EtagVersion.Any); + + result.Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); + } + + [Fact] + public async Task Should_get_latestet_version_when_requesting_state_with_auto() + { + await SetupUpdatedAsync(); + + var result = sut.GetSnapshot(EtagVersion.Auto); + + result.Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); + } + + [Fact] + public async Task Should_get_empty_version_when_requesting_state_with_empty_version() + { + await SetupUpdatedAsync(); + + var result = sut.GetSnapshot(EtagVersion.Empty); + + result.Should().BeEquivalentTo(new MyDomainState { Value = 0, Version = -1 }); + } + + [Fact] + public async Task Should_get_specific_version_when_requesting_state_with_specific_version() + { + await SetupUpdatedAsync(); + + sut.GetSnapshot(0).Should().BeEquivalentTo(new MyDomainState { Value = 4, Version = 0 }); + sut.GetSnapshot(1).Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); + } + + [Fact] + public async Task Should_get_null_state_when_requesting_state_with_invalid_version() + { + await SetupUpdatedAsync(); + + Assert.Null(sut.GetSnapshot(-4)); + Assert.Null(sut.GetSnapshot(2)); + } + + [Fact] + public void Should_instantiate() + { + Assert.Equal(EtagVersion.Empty, sut.Version); + } + + [Fact] + public async Task Should_write_state_and_events_when_created() + { + await SetupEmptyAsync(); + + var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); + + A.CallTo(() => snapshotStore.WriteAsync(id, A.That.Matches(x => x.Value == 4), -1, 0)) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) + .MustHaveHappened(); + + Assert.True(result.Value is EntityCreatedResult); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(4, sut.Snapshot.Value); + Assert.Equal(0, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_write_state_and_events_when_updated() + { + await SetupCreatedAsync(); + + var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); + + A.CallTo(() => snapshotStore.WriteAsync(id, A.That.Matches(x => x.Value == 8), 0, 1)) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) + .MustHaveHappened(); + + Assert.True(result.Value is EntitySavedResult); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(8, sut.Snapshot.Value); + Assert.Equal(1, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_throw_exception_when_already_created() + { + await SetupCreatedAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); + } + + [Fact] + public async Task Should_throw_exception_when_not_created() + { + await SetupEmptyAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); + } + + [Fact] + public async Task Should_return_custom_result_on_create() + { + await SetupEmptyAsync(); + + var result = await sut.ExecuteAsync(C(new CreateCustom())); + + Assert.Equal("CREATED", result.Value); + } + + [Fact] + public async Task Should_return_custom_result_on_update() + { + await SetupCreatedAsync(); + + var result = await sut.ExecuteAsync(C(new UpdateCustom())); + + Assert.Equal("UPDATED", result.Value); + } + + [Fact] + public async Task Should_throw_exception_when_other_verison_expected() + { + await SetupCreatedAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 }))); + } + + [Fact] + public async Task Should_reset_state_when_writing_snapshot_for_create_failed() + { + await SetupEmptyAsync(); + + A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, -1, 0)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(0, sut.Snapshot.Value); + Assert.Equal(-1, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_reset_state_when_writing_snapshot_for_update_failed() + { + await SetupCreatedAsync(); + + A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, 0, 1)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(4, sut.Snapshot.Value); + Assert.Equal(0, sut.Snapshot.Version); + } + + private async Task SetupCreatedAsync() + { + await sut.ActivateAsync(id); + + await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); + } + + private async Task SetupUpdatedAsync() + { + await SetupCreatedAsync(); + + await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); + } + + private async Task SetupEmptyAsync() + { + await sut.ActivateAsync(id); + } + + private static J C(IAggregateCommand command) + { + return command.AsJ(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs b/backend/tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeHeadersTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeHeadersTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeHeadersTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeHeadersTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs new file mode 100644 index 000000000..b76a66a62 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs @@ -0,0 +1,379 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Squidex.Infrastructure.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.EventSourcing +{ + public abstract class EventStoreTests where T : IEventStore + { + private readonly Lazy sut; + private string subscriptionPosition; + + public sealed class EventSubscriber : IEventSubscriber + { + public List Events { get; } = new List(); + + public string LastPosition { get; set; } + + public Task OnErrorAsync(IEventSubscription subscription, Exception exception) + { + throw new NotSupportedException(); + } + + public Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) + { + LastPosition = storedEvent.EventPosition; + + Events.Add(storedEvent); + + return TaskHelper.Done; + } + } + + protected T Sut + { + get { return sut.Value; } + } + + protected abstract int SubscriptionDelayInMs { get; } + + protected EventStoreTests() + { + sut = new Lazy(CreateStore); + } + + public abstract T CreateStore(); + + [Fact] + public async Task Should_throw_exception_for_version_mismatch() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + await Assert.ThrowsAsync(() => Sut.AppendAsync(Guid.NewGuid(), streamName, 0, events)); + } + + [Fact] + public async Task Should_throw_exception_for_version_mismatch_and_update() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + await Sut.AppendAsync(Guid.NewGuid(), streamName, events); + + await Assert.ThrowsAsync(() => Sut.AppendAsync(Guid.NewGuid(), streamName, 0, events)); + } + + [Fact] + public async Task Should_append_events() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + await Sut.AppendAsync(Guid.NewGuid(), streamName, events); + + var readEvents1 = await QueryAsync(streamName); + var readEvents2 = await QueryWithCallbackAsync(streamName); + + var expected = new[] + { + new StoredEvent(streamName, "Position", 0, events[0]), + new StoredEvent(streamName, "Position", 1, events[1]) + }; + + ShouldBeEquivalentTo(readEvents1, expected); + ShouldBeEquivalentTo(readEvents2, expected); + } + + [Fact] + public async Task Should_subscribe_to_events() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + var readEvents = await QueryWithSubscriptionAsync(streamName, async () => + { + await Sut.AppendAsync(Guid.NewGuid(), streamName, events); + }); + + var expected = new[] + { + new StoredEvent(streamName, "Position", 0, events[0]), + new StoredEvent(streamName, "Position", 1, events[1]) + }; + + ShouldBeEquivalentTo(readEvents, expected); + } + + [Fact] + public async Task Should_subscribe_to_next_events() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events1 = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + await QueryWithSubscriptionAsync(streamName, async () => + { + await Sut.AppendAsync(Guid.NewGuid(), streamName, events1); + }); + + var events2 = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + var readEventsFromPosition = await QueryWithSubscriptionAsync(streamName, async () => + { + await Sut.AppendAsync(Guid.NewGuid(), streamName, events2); + }); + + var expectedFromPosition = new[] + { + new StoredEvent(streamName, "Position", 2, events2[0]), + new StoredEvent(streamName, "Position", 3, events2[1]) + }; + + var readEventsFromBeginning = await QueryWithSubscriptionAsync(streamName, fromBeginning: true); + + var expectedFromBeginning = new[] + { + new StoredEvent(streamName, "Position", 0, events1[0]), + new StoredEvent(streamName, "Position", 1, events1[1]), + new StoredEvent(streamName, "Position", 2, events2[0]), + new StoredEvent(streamName, "Position", 3, events2[1]) + }; + + ShouldBeEquivalentTo(readEventsFromPosition, expectedFromPosition); + + ShouldBeEquivalentTo(readEventsFromBeginning, expectedFromBeginning); + } + + [Fact] + public async Task Should_read_events_from_offset() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + await Sut.AppendAsync(Guid.NewGuid(), streamName, events); + + var firstRead = await QueryAsync(streamName); + + var readEvents1 = await QueryAsync(streamName, 1); + var readEvents2 = await QueryWithCallbackAsync(streamName, firstRead[0].EventPosition); + + var expected = new[] + { + new StoredEvent(streamName, "Position", 1, events[1]) + }; + + ShouldBeEquivalentTo(readEvents1, expected); + ShouldBeEquivalentTo(readEvents2, expected); + } + + [Fact] + public async Task Should_delete_stream() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + await Sut.AppendAsync(Guid.NewGuid(), streamName, events); + + await Sut.DeleteStreamAsync(streamName); + + var readEvents = await QueryAsync(streamName); + + Assert.Empty(readEvents); + } + + [Fact] + public async Task Should_query_events_by_property() + { + var keyed1 = new EnvelopeHeaders(); + var keyed2 = new EnvelopeHeaders(); + + keyed1.Add("key", Guid.NewGuid().ToString()); + keyed2.Add("key", Guid.NewGuid().ToString()); + + var streamName1 = $"test-{Guid.NewGuid()}"; + var streamName2 = $"test-{Guid.NewGuid()}"; + + var events1 = new[] + { + new EventData("Type1", keyed1, "1"), + new EventData("Type2", keyed2, "2") + }; + + var events2 = new[] + { + new EventData("Type3", keyed2, "3"), + new EventData("Type4", keyed1, "4") + }; + + await Sut.CreateIndexAsync("key"); + + await Sut.AppendAsync(Guid.NewGuid(), streamName1, events1); + await Sut.AppendAsync(Guid.NewGuid(), streamName2, events2); + + var readEvents = await QueryWithFilterAsync("key", keyed2["key"].ToString()); + + var expected = new[] + { + new StoredEvent(streamName1, "Position", 1, events1[1]), + new StoredEvent(streamName2, "Position", 0, events2[0]) + }; + + ShouldBeEquivalentTo(readEvents, expected); + } + + private Task> QueryAsync(string streamName, long position = EtagVersion.Any) + { + return Sut.QueryAsync(streamName, position); + } + + private async Task?> QueryWithFilterAsync(string property, object value) + { + using (var cts = new CancellationTokenSource(30000)) + { + while (!cts.IsCancellationRequested) + { + var readEvents = new List(); + + await Sut.QueryAsync(x => { readEvents.Add(x); return TaskHelper.Done; }, property, value, null, cts.Token); + + await Task.Delay(500, cts.Token); + + if (readEvents.Count > 0) + { + return readEvents; + } + } + + cts.Token.ThrowIfCancellationRequested(); + + return null; + } + } + + private async Task?> QueryWithCallbackAsync(string? streamFilter = null, string? position = null) + { + using (var cts = new CancellationTokenSource(30000)) + { + while (!cts.IsCancellationRequested) + { + var readEvents = new List(); + + await Sut.QueryAsync(x => { readEvents.Add(x); return TaskHelper.Done; }, streamFilter, position, cts.Token); + + await Task.Delay(500, cts.Token); + + if (readEvents.Count > 0) + { + return readEvents; + } + } + + cts.Token.ThrowIfCancellationRequested(); + + return null; + } + } + + private async Task?> QueryWithSubscriptionAsync(string streamFilter, Func? action = null, bool fromBeginning = false) + { + var subscriber = new EventSubscriber(); + + IEventSubscription? subscription = null; + try + { + subscription = Sut.CreateSubscription(subscriber, streamFilter, fromBeginning ? null : subscriptionPosition); + + if (action != null) + { + await action(); + } + + using (var cts = new CancellationTokenSource(30000)) + { + while (!cts.IsCancellationRequested) + { + subscription.WakeUp(); + + await Task.Delay(500, cts.Token); + + if (subscriber.Events.Count > 0) + { + subscriptionPosition = subscriber.LastPosition; + + return subscriber.Events; + } + } + + cts.Token.ThrowIfCancellationRequested(); + + return null; + } + } + finally + { + if (subscription != null) + { + await subscription.StopAsync(); + } + } + } + + private static void ShouldBeEquivalentTo(IEnumerable? actual, params StoredEvent[] expected) + { + var actualArray = actual.Select(x => new StoredEvent(x.StreamName, "Position", x.EventStreamNumber, x.Data)).ToArray(); + + actualArray.Should().BeEquivalentTo(expected); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs new file mode 100644 index 000000000..27ecdb583 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs @@ -0,0 +1,409 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Orleans.Concurrency; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public class EventConsumerGrainTests + { + public sealed class MyEventConsumerGrain : EventConsumerGrain + { + public MyEventConsumerGrain( + EventConsumerFactory eventConsumerFactory, + IGrainState state, + IEventStore eventStore, + IEventDataFormatter eventDataFormatter, + ISemanticLog log) + : base(eventConsumerFactory, state, eventStore, eventDataFormatter, log) + { + } + + protected override IEventConsumerGrain GetSelf() + { + return this; + } + + protected override IEventSubscription CreateSubscription(IEventStore store, IEventSubscriber subscriber, string? streamFilter, string? position) + { + return store.CreateSubscription(subscriber, streamFilter, position); + } + } + + private readonly IGrainState grainState = A.Fake>(); + private readonly IEventConsumer eventConsumer = A.Fake(); + private readonly IEventStore eventStore = A.Fake(); + private readonly IEventSubscription eventSubscription = A.Fake(); + private readonly ISemanticLog log = A.Fake(); + private readonly IEventDataFormatter formatter = A.Fake(); + private readonly EventData eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); + private readonly Envelope envelope = new Envelope(new MyEvent()); + private readonly EventConsumerGrain sut; + private readonly string consumerName; + private readonly string initialPosition = Guid.NewGuid().ToString(); + + public EventConsumerGrainTests() + { + grainState.Value.Position = initialPosition; + + consumerName = eventConsumer.GetType().Name; + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) + .Returns(eventSubscription); + + A.CallTo(() => eventConsumer.Name) + .Returns(consumerName); + + A.CallTo(() => eventConsumer.Handles(A.Ignored)) + .Returns(true); + + A.CallTo(() => formatter.Parse(eventData, null)) + .Returns(envelope); + + sut = new MyEventConsumerGrain( + x => eventConsumer, + grainState, + eventStore, + formatter, + log); + } + + [Fact] + public async Task Should_not_subscribe_to_event_store_when_stopped_in_db() + { + grainState.Value = grainState.Value.Stopped(); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_subscribe_to_event_store_when_not_found_in_db() + { + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_subscribe_to_event_store_when_not_stopped_in_db() + { + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_stop_subscription_when_stopped() + { + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + await sut.StopAsync(); + await sut.StopAsync(); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_reset_consumer_when_resetting() + { + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + await sut.StopAsync(); + await sut.ResetAsync(); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = null, Error = null }); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(2, Times.Exactly); + + A.CallTo(() => eventConsumer.ClearAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, grainState.Value.Position)) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, null)) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_invoke_and_update_position_when_event_received() + { + var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnEventAsync(eventSubscription, @event); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_not_invoke_but_update_position_when_consumer_does_not_want_to_handle() + { + var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); + + A.CallTo(() => eventConsumer.Handles(@event)) + .Returns(false); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnEventAsync(eventSubscription, @event); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_ignore_old_events() + { + A.CallTo(() => formatter.Parse(eventData, null)) + .Throws(new TypeNameNotFoundException()); + + var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnEventAsync(eventSubscription, @event); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_invoke_and_update_position_when_event_is_from_another_subscription() + { + var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnEventAsync(A.Fake(), @event); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_stop_if_consumer_failed() + { + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + var ex = new InvalidOperationException(); + + await OnErrorAsync(eventSubscription, ex); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_not_make_error_handling_when_exception_is_from_another_subscription() + { + var ex = new InvalidOperationException(); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnErrorAsync(A.Fake(), ex); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); + + A.CallTo(() => grainState.WriteAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_wakeup_when_already_subscribed() + { + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + await sut.ActivateAsync(); + + A.CallTo(() => eventSubscription.WakeUp()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_stop_if_resetting_failed() + { + var ex = new InvalidOperationException(); + + A.CallTo(() => eventConsumer.ClearAsync()) + .Throws(ex); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + await sut.ResetAsync(); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_stop_if_handling_failed() + { + var ex = new InvalidOperationException(); + + A.CallTo(() => eventConsumer.On(envelope)) + .Throws(ex); + + var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnEventAsync(eventSubscription, @event); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustHaveHappened(); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_stop_if_deserialization_failed() + { + var ex = new InvalidOperationException(); + + A.CallTo(() => formatter.Parse(eventData, null)) + .Throws(ex); + + var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnEventAsync(eventSubscription, @event); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustNotHaveHappened(); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_start_after_stop_when_handling_failed() + { + var exception = new InvalidOperationException(); + + A.CallTo(() => eventConsumer.On(envelope)) + .Throws(exception); + + var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnEventAsync(eventSubscription, @event); + + await sut.StopAsync(); + await sut.StartAsync(); + await sut.StartAsync(); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustHaveHappened(); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(2, Times.Exactly); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) + .MustHaveHappened(2, Times.Exactly); + } + + private Task OnErrorAsync(IEventSubscription subscriber, Exception ex) + { + return sut.OnErrorAsync(subscriber.AsImmutable(), ex.AsImmutable()); + } + + private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev) + { + return sut.OnEventAsync(subscriber.AsImmutable(), ev.AsImmutable()); + } + } +} \ No newline at end of file diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs new file mode 100644 index 000000000..2d29eb56a --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs @@ -0,0 +1,186 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Orleans; +using Orleans.Concurrency; +using Orleans.Core; +using Orleans.Runtime; +using Xunit; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public class EventConsumerManagerGrainTests + { + public class MyEventConsumerManagerGrain : EventConsumerManagerGrain + { + public MyEventConsumerManagerGrain( + IEnumerable eventConsumers, + IGrainIdentity identity, + IGrainRuntime runtime) + : base(eventConsumers, identity, runtime) + { + } + } + + private readonly IEventConsumer consumerA = A.Fake(); + private readonly IEventConsumer consumerB = A.Fake(); + private readonly IEventConsumerGrain grainA = A.Fake(); + private readonly IEventConsumerGrain grainB = A.Fake(); + private readonly MyEventConsumerManagerGrain sut; + + public EventConsumerManagerGrainTests() + { + var grainRuntime = A.Fake(); + var grainFactory = A.Fake(); + + A.CallTo(() => grainFactory.GetGrain("a", null)).Returns(grainA); + A.CallTo(() => grainFactory.GetGrain("b", null)).Returns(grainB); + A.CallTo(() => grainRuntime.GrainFactory).Returns(grainFactory); + + A.CallTo(() => consumerA.Name).Returns("a"); + A.CallTo(() => consumerA.EventsFilter).Returns("^a-"); + + A.CallTo(() => consumerB.Name).Returns("b"); + A.CallTo(() => consumerB.EventsFilter).Returns("^b-"); + + sut = new MyEventConsumerManagerGrain(new[] { consumerA, consumerB }, A.Fake(), grainRuntime); + } + + [Fact] + public async Task Should_not_activate_all_grains_on_activate() + { + await sut.OnActivateAsync(); + + A.CallTo(() => grainA.ActivateAsync()) + .MustNotHaveHappened(); + + A.CallTo(() => grainB.ActivateAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_activate_all_grains_on_reminder() + { + await sut.ReceiveReminder(null!, default); + + A.CallTo(() => grainA.ActivateAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.ActivateAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_activate_all_grains_on_wakeup_with_null() + { + await sut.ActivateAsync(null); + + A.CallTo(() => grainA.ActivateAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.ActivateAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_activate_matching_grains_when_stream_name_defined() + { + await sut.ActivateAsync("a-123"); + + A.CallTo(() => grainA.ActivateAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.ActivateAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_start_all_grains() + { + await sut.StartAllAsync(); + + A.CallTo(() => grainA.StartAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.StartAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_start_matching_grain() + { + await sut.StartAsync("a"); + + A.CallTo(() => grainA.StartAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.StartAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_stop_all_grains() + { + await sut.StopAllAsync(); + + A.CallTo(() => grainA.StopAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.StopAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_stop_matching_grain() + { + await sut.StopAsync("b"); + + A.CallTo(() => grainA.StopAsync()) + .MustNotHaveHappened(); + + A.CallTo(() => grainB.StopAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_reset_matching_grain() + { + await sut.ResetAsync("b"); + + A.CallTo(() => grainA.ResetAsync()) + .MustNotHaveHappened(); + + A.CallTo(() => grainB.ResetAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_fetch_infos_from_all_grains() + { + A.CallTo(() => grainA.GetStateAsync()) + .Returns(new Immutable( + new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" })); + + A.CallTo(() => grainB.GetStateAsync()) + .Returns(new Immutable( + new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" })); + + var infos = await sut.GetConsumersAsync(); + + infos.Value.Should().BeEquivalentTo( + new List + { + new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" }, + new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" } + }); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs new file mode 100644 index 000000000..5564a2147 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs @@ -0,0 +1,124 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Xunit; + +namespace Squidex.Infrastructure.EventSourcing +{ + public class RetrySubscriptionTests + { + private readonly IEventStore eventStore = A.Fake(); + private readonly IEventSubscriber eventSubscriber = A.Fake(); + private readonly IEventSubscription eventSubscription = A.Fake(); + private readonly IEventSubscriber sutSubscriber; + private readonly RetrySubscription sut; + private readonly string streamFilter = Guid.NewGuid().ToString(); + + public RetrySubscriptionTests() + { + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)).Returns(eventSubscription); + + sut = new RetrySubscription(eventStore, eventSubscriber, streamFilter, null) { ReconnectWaitMs = 50 }; + + sutSubscriber = sut; + } + + [Fact] + public async Task Should_subscribe_after_constructor() + { + await sut.StopAsync(); + + A.CallTo(() => eventStore.CreateSubscription(sut, streamFilter, null)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_reopen_subscription_once_when_exception_is_retrieved() + { + await OnErrorAsync(eventSubscription, new InvalidOperationException()); + + await Task.Delay(1000); + + await sut.StopAsync(); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(2, Times.Exactly); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) + .MustHaveHappened(2, Times.Exactly); + + A.CallTo(() => eventSubscriber.OnErrorAsync(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_forward_error_from_inner_subscription_when_failed_often() + { + var ex = new InvalidOperationException(); + + await OnErrorAsync(eventSubscription, ex); + await OnErrorAsync(null!, ex); + await OnErrorAsync(null!, ex); + await OnErrorAsync(null!, ex); + await OnErrorAsync(null!, ex); + await OnErrorAsync(null!, ex); + await sut.StopAsync(); + + A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_forward_error_when_exception_is_from_another_subscription() + { + var ex = new InvalidOperationException(); + + await OnErrorAsync(A.Fake(), ex); + await sut.StopAsync(); + + A.CallTo(() => eventSubscriber.OnErrorAsync(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_forward_event_from_inner_subscription() + { + var ev = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); + + await OnEventAsync(eventSubscription, ev); + await sut.StopAsync(); + + A.CallTo(() => eventSubscriber.OnEventAsync(sut, ev)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_forward_event_when_message_is_from_another_subscription() + { + var ev = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); + + await OnEventAsync(A.Fake(), ev); + await sut.StopAsync(); + + A.CallTo(() => eventSubscriber.OnEventAsync(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + private Task OnErrorAsync(IEventSubscription subscriber, Exception ex) + { + return sutSubscriber.OnErrorAsync(subscriber, ex); + } + + private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev) + { + return sutSubscriber.OnEventAsync(subscriber, ev); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/GravatarHelperTests.cs b/backend/tests/Squidex.Infrastructure.Tests/GravatarHelperTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/GravatarHelperTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/GravatarHelperTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/GuardTests.cs b/backend/tests/Squidex.Infrastructure.Tests/GuardTests.cs new file mode 100644 index 000000000..8761ccd94 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/GuardTests.cs @@ -0,0 +1,367 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class GuardTests + { + private sealed class MyValidatableValid : IValidatable + { + public void Validate(IList errors) + { + } + } + + private sealed class MyValidatableInvalid : IValidatable + { + public void Validate(IList errors) + { + errors.Add(new ValidationError("error.", "error")); + } + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void NotNullOrEmpty_should_throw_for_empy_strings(string invalidString) + { + Assert.Throws(() => Guard.NotNullOrEmpty(invalidString, "parameter")); + } + + [Fact] + public void NotNullOrEmpty_should_throw_for_null_string() + { + Assert.Throws(() => Guard.NotNullOrEmpty(null, "parameter")); + } + + [Fact] + public void NotNullOrEmpty_should_do_nothing_for_vaid_string() + { + Guard.NotNullOrEmpty("value", "parameter"); + } + + [Fact] + public void NotNull_should_throw_for_null_value() + { + Assert.Throws(() => Guard.NotNull(null, "parameter")); + } + + [Fact] + public void NotNull_should_do_nothing_for_valid_value() + { + Guard.NotNull("value", "parameter"); + } + + [Fact] + public void Enum_should_throw_for_invalid_enum() + { + Assert.Throws(() => Guard.Enum((DateTimeKind)13, "Parameter")); + } + + [Fact] + public void Enum_should_do_nothing_for_valid_enum() + { + Guard.Enum(DateTimeKind.Local, "Parameter"); + } + + [Fact] + public void NotEmpty_should_throw_for_empty_guid() + { + Assert.Throws(() => Guard.NotEmpty(Guid.Empty, "parameter")); + } + + [Fact] + public void NotEmpty_should_do_nothing_for_valid_guid() + { + Guard.NotEmpty(Guid.NewGuid(), "parameter"); + } + + [Fact] + public void HasType_should_throw_for_other_type() + { + Assert.Throws(() => Guard.HasType("value", "parameter")); + } + + [Fact] + public void HasType_should_do_nothing_for_null_value() + { + Guard.HasType(null, "parameter"); + } + + [Fact] + public void HasType_should_do_nothing_for_correct_type() + { + Guard.HasType(123, "parameter"); + } + + [Fact] + public void HasType_nongeneric_should_throw_for_other_type() + { + Assert.Throws(() => Guard.HasType("value", typeof(int), "parameter")); + } + + [Fact] + public void HasType_nongeneric_should_do_nothing_for_null_value() + { + Guard.HasType(null, typeof(int), "parameter"); + } + + [Fact] + public void HasType_nongeneric_should_do_nothing_for_correct_type() + { + Guard.HasType(123, typeof(int), "parameter"); + } + + [Fact] + public void HasType_nongeneric_should_do_nothing_for_null_type() + { + Guard.HasType(123, null, "parameter"); + } + + [Fact] + public void NotDefault_should_throw_for_default_values() + { + Assert.Throws(() => Guard.NotDefault(Guid.Empty, "parameter")); + Assert.Throws(() => Guard.NotDefault(0, "parameter")); + Assert.Throws(() => Guard.NotDefault((string?)null, "parameter")); + Assert.Throws(() => Guard.NotDefault(false, "parameter")); + } + + [Fact] + public void NotDefault_should_do_nothing_for_non_default_value() + { + Guard.NotDefault(Guid.NewGuid(), "parameter"); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(" Not a Slug ")] + [InlineData(" not--a--slug ")] + [InlineData(" not-a-slug ")] + [InlineData("-not-a-slug-")] + [InlineData("not$-a-slug")] + [InlineData("not-a-Slug")] + public void ValidSlug_should_throw_for_invalid_slugs(string slug) + { + Assert.Throws(() => Guard.ValidSlug(slug, "parameter")); + } + + [Theory] + [InlineData("slug")] + [InlineData("slug23")] + [InlineData("other-slug")] + [InlineData("just-another-slug")] + public void ValidSlug_should_do_nothing_for_valid_slugs(string slug) + { + Guard.ValidSlug(slug, "parameter"); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(" Not a Property ")] + [InlineData(" not--a--property ")] + [InlineData(" not-a-property ")] + [InlineData("-not-a-property-")] + [InlineData("not$-a-property")] + public void ValidPropertyName_should_throw_for_invalid_slugs(string slug) + { + Assert.Throws(() => Guard.ValidPropertyName(slug, "property")); + } + + [Theory] + [InlineData("property")] + [InlineData("property23")] + [InlineData("other-property")] + [InlineData("other-Property")] + [InlineData("otherProperty")] + [InlineData("just-another-property")] + [InlineData("just-Another-Property")] + [InlineData("justAnotherProperty")] + public void ValidPropertyName_should_do_nothing_for_valid_slugs(string property) + { + Guard.ValidPropertyName(property, "parameter"); + } + + [Theory] + [InlineData(double.PositiveInfinity)] + [InlineData(double.NegativeInfinity)] + [InlineData(double.NaN)] + public void ValidNumber_should_throw_for_invalid_doubles(double value) + { + Assert.Throws(() => Guard.ValidNumber(value, "parameter")); + } + + [Theory] + [InlineData(0d)] + [InlineData(-1000d)] + [InlineData(1000d)] + public void ValidNumber_do_nothing_for_valid_double(double value) + { + Guard.ValidNumber(value, "parameter"); + } + + [Theory] + [InlineData(float.PositiveInfinity)] + [InlineData(float.NegativeInfinity)] + [InlineData(float.NaN)] + public void ValidNumber_should_throw_for_invalid_float(float value) + { + Assert.Throws(() => Guard.ValidNumber(value, "parameter")); + } + + [Theory] + [InlineData(0f)] + [InlineData(-1000f)] + [InlineData(1000f)] + public void ValidNumber_do_nothing_for_valid_float(float value) + { + Guard.ValidNumber(value, "parameter"); + } + + [Theory] + [InlineData(4)] + [InlineData(104)] + public void Between_should_throw_for_values_outside_of_range(int value) + { + Assert.Throws(() => Guard.Between(value, 10, 100, "parameter")); + } + + [Theory] + [InlineData(10)] + [InlineData(55)] + [InlineData(100)] + public void Between_should_do_nothing_for_values_in_range(int value) + { + Guard.Between(value, 10, 100, "parameter"); + } + + [Theory] + [InlineData(0)] + [InlineData(100)] + public void GreaterThan_should_throw_for_smaller_values(int value) + { + Assert.Throws(() => Guard.GreaterThan(value, 100, "parameter")); + } + + [Theory] + [InlineData(101)] + [InlineData(200)] + public void GreaterThan_should_do_nothing_for_greater_values(int value) + { + Guard.GreaterThan(value, 100, "parameter"); + } + + [Theory] + [InlineData(0)] + [InlineData(99)] + public void GreaterEquals_should_throw_for_smaller_values(int value) + { + Assert.Throws(() => Guard.GreaterEquals(value, 100, "parameter")); + } + + [Theory] + [InlineData(100)] + [InlineData(200)] + public void GreaterEquals_should_do_nothing_for_greater_values(int value) + { + Guard.GreaterEquals(value, 100, "parameter"); + } + + [Theory] + [InlineData(1000)] + [InlineData(100)] + public void LessThan_should_throw_for_greater_values(int value) + { + Assert.Throws(() => Guard.LessThan(value, 100, "parameter")); + } + + [Theory] + [InlineData(99)] + [InlineData(50)] + public void LessThan_should_do_nothing_for_smaller_values(int value) + { + Guard.LessThan(value, 100, "parameter"); + } + + [Theory] + [InlineData(1000)] + [InlineData(101)] + public void LessEquals_should_throw_for_greater_values(int value) + { + Assert.Throws(() => Guard.LessEquals(value, 100, "parameter")); + } + + [Theory] + [InlineData(100)] + [InlineData(50)] + public void LessEquals_should_do_nothing_for_smaller_values(int value) + { + Guard.LessEquals(value, 100, "parameter"); + } + + [Fact] + public void NotEmpty_should_throw_for_empty_collection() + { + Assert.Throws(() => Guard.NotEmpty(new int[0], "parameter")); + } + + [Fact] + public void NotEmpty_should_throw_for_null_collection() + { + Assert.Throws(() => Guard.NotEmpty((int[]?)null, "parameter")); + } + + [Fact] + public void NotEmpty_should_do_nothing_for_value_collection() + { + Guard.NotEmpty(new[] { 1, 2, 3 }, "parameter"); + } + + [Fact] + public void ValidFileName_should_throw_for_invalid_file_name() + { + Assert.Throws(() => Guard.ValidFileName("File/Name", "Parameter")); + } + + [Fact] + public void ValidFileName_should_throw_for_null_file_name() + { + Assert.Throws(() => Guard.ValidFileName(null, "Parameter")); + } + + [Fact] + public void ValidFileName_should_do_nothing_for_valid_file_name() + { + Guard.ValidFileName("FileName", "Parameter"); + } + + [Fact] + public void Valid_should_throw_exception_if_null() + { + Assert.Throws(() => Guard.Valid(null, "Parameter", () => "Message")); + } + + [Fact] + public void Valid_should_throw_exception_if_invalid() + { + Assert.Throws(() => Guard.Valid(new MyValidatableInvalid(), "Parameter", () => "Message")); + } + + [Fact] + public void Valid_should_do_nothing_if_valid() + { + Guard.Valid(new MyValidatableValid(), "Parameter", () => "Message"); + } + } +} \ No newline at end of file diff --git a/backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs new file mode 100644 index 000000000..060b5e4cf --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs @@ -0,0 +1,131 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using Xunit; + +#pragma warning disable SA1122 // Use string.Empty for empty strings + +namespace Squidex.Infrastructure.Http +{ + public class DumpFormatterTests + { + [Fact] + public void Should_format_dump_without_response() + { + var httpRequest = CreateRequest(); + + var dump = DumpFormatter.BuildDump(httpRequest, null, null, null, TimeSpan.FromMinutes(1), true); + + var expected = CreateExpectedDump( + "Request:", + "POST: https://cloud.squidex.io/ HTTP/1.1", + "User-Agent: Squidex/1.0", + "Accept-Language: de; en", + "Accept-Encoding: UTF-8", + "", + "", + "Response:", + "Timeout after 00:01:00"); + + Assert.Equal(expected, dump); + } + + [Fact] + public void Should_format_dump_without_content() + { + var httpRequest = CreateRequest(); + var httpResponse = CreateResponse(); + + var dump = DumpFormatter.BuildDump(httpRequest, httpResponse, null, null, TimeSpan.FromMinutes(1), false); + + var expected = CreateExpectedDump( + "Request:", + "POST: https://cloud.squidex.io/ HTTP/1.1", + "User-Agent: Squidex/1.0", + "Accept-Language: de; en", + "Accept-Encoding: UTF-8", + "", + "", + "Response:", + "HTTP/1.1 200 OK", + "Transfer-Encoding: UTF-8", + "Trailer: Expires", + "", + "Elapsed: 00:01:00"); + + Assert.Equal(expected, dump); + } + + [Fact] + public void Should_format_dump_with_content_without_timeout() + { + var httpRequest = CreateRequest(new StringContent("Hello Squidex", Encoding.UTF8, "text/plain")); + var httpResponse = CreateResponse(new StringContent("Hello Back", Encoding.UTF8, "text/plain")); + + var dump = DumpFormatter.BuildDump(httpRequest, httpResponse, "Hello Squidex", "Hello Back", TimeSpan.FromMinutes(1), false); + + var expected = CreateExpectedDump( + "Request:", + "POST: https://cloud.squidex.io/ HTTP/1.1", + "User-Agent: Squidex/1.0", + "Accept-Language: de; en", + "Accept-Encoding: UTF-8", + "Content-Type: text/plain; charset=utf-8", + "", + "Hello Squidex", + "", + "", + "Response:", + "HTTP/1.1 200 OK", + "Transfer-Encoding: UTF-8", + "Trailer: Expires", + "Content-Type: text/plain; charset=utf-8", + "", + "Hello Back", + "", + "Elapsed: 00:01:00"); + + Assert.Equal(expected, dump); + } + + private static HttpRequestMessage CreateRequest(HttpContent? content = null) + { + var request = new HttpRequestMessage(HttpMethod.Post, new Uri("https://cloud.squidex.io")); + + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("Squidex", "1.0")); + request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("de")); + request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("en")); + request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("UTF-8")); + + request.Content = content; + + return request; + } + + private static HttpResponseMessage CreateResponse(HttpContent? content = null) + { + var response = new HttpResponseMessage(HttpStatusCode.OK); + + response.Headers.TransferEncoding.Add(new TransferCodingHeaderValue("UTF-8")); + response.Headers.Trailer.Add("Expires"); + + response.Content = content; + + return response; + } + + private static string CreateExpectedDump(params string[] input) + { + return string.Join(Environment.NewLine, input) + Environment.NewLine; + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs b/backend/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/InstantExtensions.cs rename to backend/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs new file mode 100644 index 000000000..90886125b --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using System.Security.Claims; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Json +{ + public class ClaimsPrincipalConverterTests + { + [Fact] + public void Should_serialize_and_deserialize() + { + var value = new ClaimsPrincipal( + new[] + { + new ClaimsIdentity( + new[] + { + new Claim("email", "me@email.com"), + new Claim("username", "me@email.com") + }, + "Cookie"), + new ClaimsIdentity( + new[] + { + new Claim("user_id", "12345"), + new Claim("login", "me") + }, + "Google") + }); + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value.Identities.ElementAt(0).AuthenticationType, serialized.Identities.ElementAt(0).AuthenticationType); + Assert.Equal(value.Identities.ElementAt(1).AuthenticationType, serialized.Identities.ElementAt(1).AuthenticationType); + } + + [Fact] + public void Should_serialize_and_deserialize_null_principal() + { + ClaimsPrincipal? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Null(serialized); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs new file mode 100644 index 000000000..e6706d506 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs @@ -0,0 +1,357 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NodaTime; +using Xunit; + +namespace Squidex.Infrastructure.Json.Objects +{ + public class JsonObjectTests + { + [Fact] + public void Should_make_correct_object_equal_comparisons() + { + var obj_count1_key1_val1_a = JsonValue.Object().Add("key1", 1); + var obj_count1_key1_val1_b = JsonValue.Object().Add("key1", 1); + + var obj_count1_key1_val2 = JsonValue.Object().Add("key1", 2); + var obj_count1_key2_val1 = JsonValue.Object().Add("key2", 1); + var obj_count2_key1_val1 = JsonValue.Object().Add("key1", 1).Add("key2", 2); + + var number = JsonValue.Create(1); + + Assert.Equal(obj_count1_key1_val1_a, obj_count1_key1_val1_b); + Assert.Equal(obj_count1_key1_val1_a.GetHashCode(), obj_count1_key1_val1_b.GetHashCode()); + Assert.True(obj_count1_key1_val1_a.Equals((object)obj_count1_key1_val1_b)); + + Assert.NotEqual(obj_count1_key1_val1_a, obj_count1_key1_val2); + Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), obj_count1_key1_val2.GetHashCode()); + Assert.False(obj_count1_key1_val1_a.Equals((object)obj_count1_key1_val2)); + + Assert.NotEqual(obj_count1_key1_val1_a, obj_count1_key2_val1); + Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), obj_count1_key2_val1.GetHashCode()); + Assert.False(obj_count1_key1_val1_a.Equals((object)obj_count1_key2_val1)); + + Assert.NotEqual(obj_count1_key1_val1_a, obj_count2_key1_val1); + Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), obj_count2_key1_val1.GetHashCode()); + Assert.False(obj_count1_key1_val1_a.Equals((object)obj_count2_key1_val1)); + + Assert.NotEqual(obj_count1_key1_val1_a, number); + Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), number.GetHashCode()); + Assert.False(obj_count1_key1_val1_a.Equals((object)number)); + } + + [Fact] + public void Should_make_correct_array_equal_comparisons() + { + var array_count1_val1_a = JsonValue.Array(1); + var array_count1_val1_b = JsonValue.Array(1); + + var array_count1_val2 = JsonValue.Array(2); + var array_count2_val1 = JsonValue.Array(1, 2); + + var number = JsonValue.Create(1); + + Assert.Equal(array_count1_val1_a, array_count1_val1_b); + Assert.Equal(array_count1_val1_a.GetHashCode(), array_count1_val1_b.GetHashCode()); + Assert.True(array_count1_val1_a.Equals((object)array_count1_val1_b)); + + Assert.NotEqual(array_count1_val1_a, array_count1_val2); + Assert.NotEqual(array_count1_val1_a.GetHashCode(), array_count1_val2.GetHashCode()); + Assert.False(array_count1_val1_a.Equals((object)array_count1_val2)); + + Assert.NotEqual(array_count1_val1_a, array_count2_val1); + Assert.NotEqual(array_count1_val1_a.GetHashCode(), array_count2_val1.GetHashCode()); + Assert.False(array_count1_val1_a.Equals((object)array_count2_val1)); + + Assert.NotEqual(array_count1_val1_a, number); + Assert.NotEqual(array_count1_val1_a.GetHashCode(), number.GetHashCode()); + Assert.False(array_count1_val1_a.Equals((object)number)); + } + + [Fact] + public void Should_make_correct_array_scalar_comparisons() + { + var number_val1_a = JsonValue.Create(1); + var number_val1_b = JsonValue.Create(1); + + var number_val2 = JsonValue.Create(2); + + var boolean = JsonValue.True; + + Assert.Equal(number_val1_a, number_val1_b); + Assert.Equal(number_val1_a.GetHashCode(), number_val1_b.GetHashCode()); + Assert.True(number_val1_a.Equals((object)number_val1_b)); + + Assert.NotEqual(number_val1_a, number_val2); + Assert.NotEqual(number_val1_a.GetHashCode(), number_val2.GetHashCode()); + Assert.False(number_val1_a.Equals((object)number_val2)); + + Assert.NotEqual(number_val1_a, boolean); + Assert.NotEqual(number_val1_a.GetHashCode(), boolean.GetHashCode()); + Assert.False(number_val1_a.Equals((object)boolean)); + } + + [Fact] + public void Should_make_correct_null_comparisons() + { + var null_a = JsonValue.Null; + var null_b = JsonValue.Null; + + var boolean = JsonValue.True; + + Assert.Equal(null_a, null_b); + Assert.Equal(null_a.GetHashCode(), null_b.GetHashCode()); + Assert.True(null_a.Equals((object)null_b)); + + Assert.NotEqual(null_a, boolean); + Assert.NotEqual(null_a.GetHashCode(), boolean.GetHashCode()); + Assert.False(null_a.Equals((object)boolean)); + } + + [Fact] + public void Should_cache_null() + { + Assert.Same(JsonValue.Null, JsonValue.Create((string?)null)); + Assert.Same(JsonValue.Null, JsonValue.Create((bool?)null)); + Assert.Same(JsonValue.Null, JsonValue.Create((double?)null)); + Assert.Same(JsonValue.Null, JsonValue.Create((object?)null)); + Assert.Same(JsonValue.Null, JsonValue.Create((Instant?)null)); + } + + [Fact] + public void Should_cache_true() + { + Assert.Same(JsonValue.True, JsonValue.Create(true)); + } + + [Fact] + public void Should_cache_false() + { + Assert.Same(JsonValue.False, JsonValue.Create(false)); + } + + [Fact] + public void Should_cache_empty() + { + Assert.Same(JsonValue.Empty, JsonValue.Create(string.Empty)); + } + + [Fact] + public void Should_cache_zero() + { + Assert.Same(JsonValue.Zero, JsonValue.Create(0)); + } + + [Fact] + public void Should_boolean_from_object() + { + Assert.Equal(JsonValue.True, JsonValue.Create((object)true)); + } + + [Fact] + public void Should_create_value_from_instant() + { + var instant = Instant.FromUnixTimeSeconds(4123125455); + + Assert.Equal(instant.ToString(), JsonValue.Create(instant).ToString()); + } + + [Fact] + public void Should_create_value_from_instant_object() + { + var instant = Instant.FromUnixTimeSeconds(4123125455); + + Assert.Equal(instant.ToString(), JsonValue.Create((object)instant).ToString()); + } + + [Fact] + public void Should_create_array() + { + var json = JsonValue.Array(1, "2"); + + Assert.Equal("[1, \"2\"]", json.ToJsonString()); + Assert.Equal("[1, \"2\"]", json.ToString()); + } + + [Fact] + public void Should_create_object() + { + var json = JsonValue.Object().Add("key1", 1).Add("key2", "2"); + + Assert.Equal("{\"key1\":1, \"key2\":\"2\"}", json.ToJsonString()); + Assert.Equal("{\"key1\":1, \"key2\":\"2\"}", json.ToString()); + } + + [Fact] + public void Should_create_number() + { + var json = JsonValue.Create(123); + + Assert.Equal("123", json.ToJsonString()); + Assert.Equal("123", json.ToString()); + } + + [Fact] + public void Should_create_boolean_true() + { + var json = JsonValue.Create(true); + + Assert.Equal("true", json.ToJsonString()); + Assert.Equal("true", json.ToString()); + } + + [Fact] + public void Should_create_boolean_false() + { + var json = JsonValue.Create(false); + + Assert.Equal("false", json.ToJsonString()); + Assert.Equal("false", json.ToString()); + } + + [Fact] + public void Should_create_string() + { + var json = JsonValue.Create("hi"); + + Assert.Equal("\"hi\"", json.ToJsonString()); + Assert.Equal("hi", json.ToString()); + } + + [Fact] + public void Should_create_null() + { + var json = JsonValue.Create((object?)null); + + Assert.Equal("null", json.ToJsonString()); + Assert.Equal("null", json.ToString()); + } + + [Fact] + public void Should_create_arrays_in_different_ways() + { + var numbers = new[] + { + JsonValue.Array(1.0f, 2.0f), + JsonValue.Array(JsonValue.Create(1.0f), JsonValue.Create(2.0f)) + }; + + Assert.Single(numbers.Distinct()); + Assert.Single(numbers.Select(x => x.GetHashCode()).Distinct()); + } + + [Fact] + public void Should_create_number_from_types() + { + var numbers = new[] + { + JsonValue.Create(12.0f), + JsonValue.Create(12.0), + JsonValue.Create(12L), + JsonValue.Create(12), + JsonValue.Create((object)12.0d), + JsonValue.Create((double?)12.0d) + }; + + Assert.Single(numbers.Distinct()); + Assert.Single(numbers.Select(x => x.GetHashCode()).Distinct()); + } + + [Fact] + public void Should_create_null_when_adding_null_to_array() + { + var array = JsonValue.Array(); + + array.Add(null!); + + Assert.Same(JsonValue.Null, array[0]); + } + + [Fact] + public void Should_create_null_when_replacing_to_null_in_array() + { + var array = JsonValue.Array(1); + + array[0] = null!; + + Assert.Same(JsonValue.Null, array[0]); + } + + [Fact] + public void Should_create_null_when_adding_null_to_object() + { + var obj = JsonValue.Object(); + + obj.Add("key", null!); + + Assert.Same(JsonValue.Null, obj["key"]); + } + + [Fact] + public void Should_create_null_when_replacing_to_null_object() + { + var obj = JsonValue.Object(); + + obj["key"] = null!; + + Assert.Same(JsonValue.Null, obj["key"]); + } + + [Fact] + public void Should_remove_value_from_object() + { + var obj = JsonValue.Object().Add("key", 1); + + obj.Remove("key"); + + Assert.False(obj.TryGetValue("key", out _)); + Assert.False(obj.ContainsKey("key")); + } + + [Fact] + public void Should_clear_values_from_object() + { + var obj = JsonValue.Object().Add("key", 1); + + obj.Clear(); + + Assert.False(obj.TryGetValue("key", out _)); + Assert.False(obj.ContainsKey("key")); + } + + [Fact] + public void Should_provide_collection_values_from_object() + { + var obj = JsonValue.Object().Add("11", "44").Add("22", "88"); + + var kvps = new[] + { + new KeyValuePair("11", JsonValue.Create("44")), + new KeyValuePair("22", JsonValue.Create("88")) + }; + + Assert.Equal(2, obj.Count); + + Assert.Equal(new[] { "11", "22" }, obj.Keys); + Assert.Equal(new[] { "44", "88" }, obj.Values.Select(x => x.ToString())); + + Assert.Equal(kvps, obj.ToArray()); + Assert.Equal(kvps, ((IEnumerable)obj).OfType>().ToArray()); + } + + [Fact] + public void Should_throw_exception_when_creation_value_from_invalid_type() + { + Assert.Throws(() => JsonValue.Create(Guid.Empty)); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs b/backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs new file mode 100644 index 000000000..c49223413 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs @@ -0,0 +1,141 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class LanguageTests + { + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Should_throw_exception_if_getting_by_empty_key(string key) + { + Assert.Throws(() => Language.GetLanguage(key)); + } + + [Fact] + public void Should_throw_exception_if_getting_by_null_key() + { + Assert.Throws(() => Language.GetLanguage(null!)); + } + + [Fact] + public void Should_throw_exception_if_getting_by_unsupported_language() + { + Assert.Throws(() => Language.GetLanguage("xy")); + } + + [Fact] + public void Should_provide_all_languages() + { + Assert.True(Language.AllLanguages.Count > 100); + } + + [Fact] + public void Should_return_true_for_valid_language() + { + Assert.True(Language.IsValidLanguage("de")); + } + + [Fact] + public void Should_return_false_for_invalid_language() + { + Assert.False(Language.IsValidLanguage("xx")); + } + + [Fact] + public void Should_make_implicit_conversion_to_language() + { + Language language = "de"!; + + Assert.Equal(Language.DE, language); + } + + [Fact] + public void Should_make_implicit_conversion_to_string() + { + string iso2Code = Language.DE!; + + Assert.Equal("de", iso2Code); + } + + [Theory] + [InlineData("de", "German")] + [InlineData("en", "English")] + [InlineData("sv", "Swedish")] + [InlineData("zh", "Chinese")] + public void Should_provide_correct_english_name(string key, string englishName) + { + var language = Language.GetLanguage(key); + + Assert.Equal(key, language.Iso2Code); + Assert.Equal(englishName, language.EnglishName); + Assert.Equal(englishName, language.ToString()); + } + + [Theory] + [InlineData("en", "en")] + [InlineData("en ", "en")] + [InlineData("EN", "en")] + [InlineData("EN ", "en")] + public void Should_parse_valid_languages(string input, string languageCode) + { + var language = Language.ParseOrNull(input); + + Assert.Equal(language, Language.GetLanguage(languageCode)); + } + + [Theory] + [InlineData("en-US", "en")] + [InlineData("en-GB", "en")] + [InlineData("EN-US", "en")] + [InlineData("EN-GB", "en")] + public void Should_parse_lanuages_from_culture(string input, string languageCode) + { + var language = Language.ParseOrNull(input); + + Assert.Equal(language, Language.GetLanguage(languageCode)); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("xx")] + [InlineData("invalid")] + [InlineData(null)] + public void Should_parse_invalid_languages(string input) + { + var language = Language.ParseOrNull(input); + + Assert.Null(language); + } + + [Fact] + public void Should_serialize_and_deserialize_null_language() + { + Language? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_valid_language() + { + var value = Language.DE; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs new file mode 100644 index 000000000..e83ee7c60 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Squidex.Infrastructure +{ + public sealed class LanguagesInitializerTests + { + [Fact] + public async Task Should_add_custom_languages() + { + var options = Options.Create(new LanguagesOptions + { + ["en-NO"] = "English (Norwegian)" + }); + + var sut = new LanguagesInitializer(options); + + await sut.InitializeAsync(); + + Assert.Equal("English (Norwegian)", Language.GetLanguage("en-NO").EnglishName); + } + + [Fact] + public async Task Should_not_add_invalid_languages() + { + var options = Options.Create(new LanguagesOptions + { + ["en-Error"] = null! + }); + + var sut = new LanguagesInitializer(options); + + await sut.InitializeAsync(); + + Assert.False(Language.TryGetLanguage("en-Error", out _)); + } + + [Fact] + public async Task Should_not_override_existing_languages() + { + var options = Options.Create(new LanguagesOptions + { + ["de"] = "German (Germany)" + }); + + var sut = new LanguagesInitializer(options); + + await sut.InitializeAsync(); + + Assert.Equal("German", Language.GetLanguage("de").EnglishName); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs new file mode 100644 index 000000000..a26190676 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs @@ -0,0 +1,87 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Infrastructure.Orleans; +using Xunit; + +namespace Squidex.Infrastructure.Log +{ + public class LockingLogStoreTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly ILockGrain lockGrain = A.Fake(); + private readonly ILogStore inner = A.Fake(); + private readonly LockingLogStore sut; + + public LockingLogStoreTests() + { + A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) + .Returns(lockGrain); + + sut = new LockingLogStore(inner, grainFactory); + } + + [Fact] + public async Task Should_lock_and_call_inner() + { + var stream = new MemoryStream(); + + var dateFrom = DateTime.Today; + var dateTo = dateFrom.AddDays(2); + + var key = "MyKey"; + + var releaseToken = Guid.NewGuid().ToString(); + + A.CallTo(() => lockGrain.AcquireLockAsync(key)) + .Returns(releaseToken); + + await sut.ReadLogAsync(key, dateFrom, dateTo, stream); + + A.CallTo(() => lockGrain.AcquireLockAsync(key)) + .MustHaveHappened(); + + A.CallTo(() => lockGrain.ReleaseLockAsync(releaseToken)) + .MustHaveHappened(); + + A.CallTo(() => inner.ReadLogAsync(key, dateFrom, dateTo, stream)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_write_default_message_if_lock_could_not_be_acquired() + { + var stream = new MemoryStream(); + + var dateFrom = DateTime.Today; + var dateTo = dateFrom.AddDays(2); + + var key = "MyKey"; + + A.CallTo(() => lockGrain.AcquireLockAsync(key)) + .Returns(Task.FromResult(null)); + + await sut.ReadLogAsync(key, dateFrom, dateTo, stream, TimeSpan.FromSeconds(1)); + + A.CallTo(() => lockGrain.AcquireLockAsync(key)) + .MustHaveHappened(); + + A.CallTo(() => lockGrain.ReleaseLockAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => inner.ReadLogAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + + Assert.True(stream.Length > 0); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Log/SemanticLogAdapterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Log/SemanticLogAdapterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Log/SemanticLogAdapterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Log/SemanticLogAdapterTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs new file mode 100644 index 000000000..7f00d407a --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs @@ -0,0 +1,525 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using FakeItEasy; +using Microsoft.Extensions.Logging; +using NodaTime; +using Squidex.Infrastructure.Log.Adapter; +using Xunit; + +namespace Squidex.Infrastructure.Log +{ + public class SemanticLogTests + { + private readonly List appenders = new List(); + private readonly List channels = new List(); + private readonly Lazy log; + private readonly ILogChannel channel = A.Fake(); + private string output = string.Empty; + + public SemanticLog Log + { + get { return log.Value; } + } + + public SemanticLogTests() + { + channels.Add(channel); + + A.CallTo(() => channel.Log(A.Ignored, A.Ignored)) + .Invokes((SemanticLogLevel level, string message) => + { + output += message; + }); + + log = new Lazy(() => new SemanticLog(channels, appenders, JsonLogWriterFactory.Default())); + } + + [Fact] + public void Should_log_multiple_lines() + { + Log.Log(SemanticLogLevel.Error, None.Value, (_, w) => w.WriteProperty("logMessage", "Msg1")); + Log.Log(SemanticLogLevel.Error, None.Value, (_, w) => w.WriteProperty("logMessage", "Msg2")); + + var expected1 = + LogTest(w => w + .WriteProperty("logLevel", "Error") + .WriteProperty("logMessage", "Msg1")); + + var expected2 = + LogTest(w => w + .WriteProperty("logLevel", "Error") + .WriteProperty("logMessage", "Msg2")); + + Assert.Equal(expected1 + expected2, output); + } + + [Fact] + public void Should_log_timestamp() + { + var clock = A.Fake(); + + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); + + appenders.Add(new TimestampLogAppender(clock)); + + Log.LogFatal(w => { /* Do Nothing */ }); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteProperty("timestamp", clock.GetCurrentInstant())); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_values_with_appender() + { + appenders.Add(new ConstantsLogWriter(w => w.WriteProperty("logValue", 1500))); + + Log.LogFatal(m => { }); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_application_info() + { + var sessionId = Guid.NewGuid(); + + appenders.Add(new ApplicationInfoLogAppender(GetType(), sessionId)); + + Log.LogFatal(m => { }); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteObject("app", a => a + .WriteProperty("name", "Squidex.Infrastructure.Tests") + .WriteProperty("version", "1.0.0.0") + .WriteProperty("sessionId", sessionId.ToString()))); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_trace() + { + Log.LogTrace(w => w.WriteProperty("logValue", 1500)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Trace") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_trace_and_context() + { + Log.LogTrace(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Trace") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_debug() + { + Log.LogDebug(w => w.WriteProperty("logValue", 1500)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Debug") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_debug_and_context() + { + Log.LogDebug(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Debug") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_information() + { + Log.LogInformation(w => w.WriteProperty("logValue", 1500)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Information") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_information_and_context() + { + Log.LogInformation(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Information") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_warning() + { + Log.LogWarning(w => w.WriteProperty("logValue", 1500)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Warning") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_warning_and_context() + { + Log.LogWarning(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Warning") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_warning_exception() + { + var exception = new InvalidOperationException(); + + Log.LogWarning(exception); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Warning") + .WriteException(exception)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_warning_exception_and_context() + { + var exception = new InvalidOperationException(); + + Log.LogWarning(exception, 1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Warning") + .WriteProperty("logValue", 1500) + .WriteException(exception)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_error() + { + Log.LogError(w => w.WriteProperty("logValue", 1500)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Error") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_error_and_context() + { + Log.LogError(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Error") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_error_exception() + { + var exception = new InvalidOperationException(); + + Log.LogError(exception); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Error") + .WriteException(exception)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_error_exception_and_context() + { + var exception = new InvalidOperationException(); + + Log.LogError(exception, 1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Error") + .WriteProperty("logValue", 1500) + .WriteException(exception)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_fatal() + { + Log.LogFatal(w => w.WriteProperty("logValue", 1500)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_fatal_and_context() + { + Log.LogFatal(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_fatal_exception() + { + var exception = new InvalidOperationException(); + + Log.LogFatal(exception); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteException(exception)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_fatal_exception_and_context() + { + var exception = new InvalidOperationException(); + + Log.LogFatal(exception, 1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteProperty("logValue", 1500) + .WriteException(exception)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_nothing_when_exception_is_null() + { + Log.LogFatal((Exception?)null); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal")); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_measure_trace() + { + Log.MeasureTrace(w => w.WriteProperty("message", "My Message")).Dispose(); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Trace") + .WriteProperty("message", "My Message") + .WriteProperty("elapsedMs", 0)); + + Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); + } + + [Fact] + public void Should_measure_trace_with_contex() + { + Log.MeasureTrace("My Message", (ctx, w) => w.WriteProperty("message", ctx)).Dispose(); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Trace") + .WriteProperty("message", "My Message") + .WriteProperty("elapsedMs", 0)); + + Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); + } + + [Fact] + public void Should_measure_debug() + { + Log.MeasureDebug(w => w.WriteProperty("message", "My Message")).Dispose(); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Debug") + .WriteProperty("message", "My Message") + .WriteProperty("elapsedMs", 0)); + + Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); + } + + [Fact] + public void Should_measure_debug_with_contex() + { + Log.MeasureDebug("My Message", (ctx, w) => w.WriteProperty("message", ctx)).Dispose(); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Debug") + .WriteProperty("message", "My Message") + .WriteProperty("elapsedMs", 0)); + + Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); + } + + [Fact] + public void Should_measure_information() + { + Log.MeasureInformation(w => w.WriteProperty("message", "My Message")).Dispose(); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Information") + .WriteProperty("message", "My Message") + .WriteProperty("elapsedMs", 0)); + + Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); + } + + [Fact] + public void Should_measure_information_with_contex() + { + Log.MeasureInformation("My Message", (ctx, w) => w.WriteProperty("message", ctx)).Dispose(); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Information") + .WriteProperty("message", "My Message") + .WriteProperty("elapsedMs", 0)); + + Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); + } + + [Fact] + public void Should_log_with_extensions_logger() + { + var exception = new InvalidOperationException(); + + var loggerFactory = + new LoggerFactory() + .AddSemanticLog(Log); + var loggerInstance = loggerFactory.CreateLogger(); + + loggerInstance.LogCritical(new EventId(123, "EventName"), exception, "Log {0}", 123); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteProperty("message", "Log 123") + .WriteObject("eventId", e => e + .WriteProperty("id", 123) + .WriteProperty("name", "EventName")) + .WriteException(exception) + .WriteProperty("category", "Squidex.Infrastructure.Log.SemanticLogTests")); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_catch_all_exceptions_from_all_channels_when_exceptions_are_thrown() + { + var exception1 = new InvalidOperationException(); + var exception2 = new InvalidOperationException(); + + var channel1 = A.Fake(); + var channel2 = A.Fake(); + + A.CallTo(() => channel1.Log(A.Ignored, A.Ignored)).Throws(exception1); + A.CallTo(() => channel2.Log(A.Ignored, A.Ignored)).Throws(exception2); + + var sut = new SemanticLog(new[] { channel1, channel2 }, Enumerable.Empty(), JsonLogWriterFactory.Default()); + + try + { + sut.Log(SemanticLogLevel.Debug, None.Value, (_, w) => w.WriteProperty("should", "throw")); + + Assert.False(true); + } + catch (AggregateException ex) + { + Assert.Equal(exception1, ex.InnerExceptions[0]); + Assert.Equal(exception2, ex.InnerExceptions[1]); + } + } + + private static string LogTest(Action writer) + { + var sut = JsonLogWriterFactory.Default().Create(); + + writer(sut); + + return sut.ToString(); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs new file mode 100644 index 000000000..674de68bd --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs @@ -0,0 +1,167 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.Migrations +{ + public class MigratorTests + { + private readonly IMigrationStatus status = A.Fake(); + private readonly IMigrationPath path = A.Fake(); + private readonly ISemanticLog log = A.Fake(); + private readonly List<(int From, int To, IMigration Migration)> migrations = new List<(int From, int To, IMigration Migration)>(); + + public sealed class InMemoryStatus : IMigrationStatus + { + private readonly object lockObject = new object(); + private int version; + private bool isLocked; + + public Task GetVersionAsync() + { + return Task.FromResult(version); + } + + public Task TryLockAsync() + { + var lockAcquired = false; + + lock (lockObject) + { + if (!isLocked) + { + isLocked = true; + + lockAcquired = true; + } + } + + return Task.FromResult(lockAcquired); + } + + public Task UnlockAsync(int newVersion) + { + lock (lockObject) + { + isLocked = false; + + version = newVersion; + } + + return TaskHelper.Done; + } + } + + public MigratorTests() + { + A.CallTo(() => path.GetNext(A.Ignored)) + .ReturnsLazily((int v) => + { + var m = migrations.Where(x => x.From == v).ToList(); + + return m.Count == 0 ? (0, null) : (migrations.Max(x => x.To), migrations.Select(x => x.Migration)); + }); + + A.CallTo(() => status.GetVersionAsync()).Returns(0); + A.CallTo(() => status.TryLockAsync()).Returns(true); + } + + [Fact] + public async Task Should_migrate_step_by_step() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(1, 2); + var migrator_2_3 = BuildMigration(2, 3); + + var sut = new Migrator(status, path, log); + + await sut.MigrateAsync(); + + A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_2_3.UpdateAsync()).MustHaveHappened(); + + A.CallTo(() => status.UnlockAsync(3)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_unlock_when_migration_failed() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(1, 2); + var migrator_2_3 = BuildMigration(2, 3); + + var sut = new Migrator(status, path, log); + + A.CallTo(() => migrator_1_2.UpdateAsync()).Throws(new ArgumentException()); + + await Assert.ThrowsAsync(() => sut.MigrateAsync()); + + A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_2_3.UpdateAsync()).MustNotHaveHappened(); + + A.CallTo(() => status.UnlockAsync(0)).MustHaveHappened(); + } + + [Fact] + public async Task Should_log_exception_when_migration_failed() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(1, 2); + + var ex = new InvalidOperationException(); + + A.CallTo(() => migrator_0_1.UpdateAsync()) + .Throws(ex); + + var sut = new Migrator(status, path, log); + + await Assert.ThrowsAsync(() => sut.MigrateAsync()); + + A.CallTo(() => log.Log(SemanticLogLevel.Fatal, None.Value, A>.Ignored)) + .MustHaveHappened(); + + A.CallTo(() => migrator_1_2.UpdateAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_prevent_multiple_updates() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(1, 2); + + var sut = new Migrator(new InMemoryStatus(), path, log) { LockWaitMs = 2 }; + + await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(() => sut.MigrateAsync()))); + + A.CallTo(() => migrator_0_1.UpdateAsync()) + .MustHaveHappened(1, Times.Exactly); + A.CallTo(() => migrator_1_2.UpdateAsync()) + .MustHaveHappened(1, Times.Exactly); + } + + private IMigration BuildMigration(int fromVersion, int toVersion) + { + var migration = A.Fake(); + + migrations.Add((fromVersion, toVersion, migration)); + + return migration; + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/MongoDb/BsonConverterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonConverterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/MongoDb/BsonConverterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonConverterTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs new file mode 100644 index 000000000..030a6fbb7 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs @@ -0,0 +1,169 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Infrastructure.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.MongoDb +{ + public class MongoExtensionsTests + { + public sealed class Cursor : IAsyncCursor where T : notnull + { + private readonly List items = new List(); + private int index = -1; + + public IEnumerable Current + { + get + { + if (items[index] is Exception ex) + { + throw ex; + } + + return Enumerable.Repeat((T)items[index], 1); + } + } + + public Cursor Add(params T[] newItems) + { + foreach (var item in newItems) + { + items.Add(item); + } + + return this; + } + + public Cursor Add(Exception ex) + { + items.Add(ex); + + return this; + } + + public void Dispose() + { + } + + public bool MoveNext(CancellationToken cancellationToken = default) + { + index++; + + return index < items.Count; + } + + public async Task MoveNextAsync(CancellationToken cancellationToken = default) + { + await Task.Delay(1, cancellationToken); + + return MoveNext(cancellationToken); + } + } + + [Fact] + public async Task Should_enumerate_over_items() + { + var result = new List(); + + var cursor = new Cursor().Add(0, 1, 2, 3, 4, 5); + + await cursor.ForEachPipelineAsync(x => + { + result.Add(x); + return TaskHelper.Done; + }); + + Assert.Equal(new List { 0, 1, 2, 3, 4, 5 }, result); + } + + [Fact] + public async Task Should_break_when_cursor_failed() + { + var ex = new InvalidOperationException(); + + var result = new List(); + + using (var cursor = new Cursor().Add(0, 1, 2).Add(ex).Add(3, 4, 5)) + { + await Assert.ThrowsAsync(() => + { + return cursor.ForEachPipelineAsync(x => + { + result.Add(x); + return TaskHelper.Done; + }); + }); + } + + Assert.Equal(new List { 0, 1, 2 }, result); + } + + [Fact] + public async Task Should_break_when_handler_failed() + { + var ex = new InvalidOperationException(); + + var result = new List(); + + using (var cursor = new Cursor().Add(0, 1, 2, 3, 4, 5)) + { + await Assert.ThrowsAsync(() => + { + return cursor.ForEachPipelineAsync(x => + { + if (x == 2) + { + throw ex; + } + + result.Add(x); + return TaskHelper.Done; + }); + }); + } + + Assert.Equal(new List { 0, 1 }, result); + } + + [Fact] + public async Task Should_stop_when_cancelled1() + { + using (var cts = new CancellationTokenSource()) + { + var result = new List(); + + using (var cursor = new Cursor().Add(0, 1, 2, 3, 4, 5)) + { + await Assert.ThrowsAnyAsync(() => + { + return cursor.ForEachPipelineAsync(x => + { + if (x == 2) + { + cts.Cancel(); + } + + result.Add(x); + + return TaskHelper.Done; + }, cts.Token); + }); + } + + Assert.Equal(new List { 0, 1, 2 }, result); + } + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs b/backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs new file mode 100644 index 000000000..add1d6d8e --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs @@ -0,0 +1,140 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class NamedIdTests + { + [Fact] + public void Should_instantiate_token() + { + var id = Guid.NewGuid(); + + var namedId = NamedId.Of(id, "my-name"); + + Assert.Equal(id, namedId.Id); + Assert.Equal("my-name", namedId.Name); + } + + [Fact] + public void Should_convert_named_id_to_string() + { + var id = Guid.NewGuid(); + + var namedId = NamedId.Of(id, "my-name"); + + Assert.Equal($"{id},my-name", namedId.ToString()); + } + + [Fact] + public void Should_make_correct_equal_comparisons() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var named_id1_name1_a = NamedId.Of(id1, "name1"); + var named_id1_name1_b = NamedId.Of(id1, "name1"); + + var named_id2_name1 = NamedId.Of(id2, "name1"); + var named_id1_name2 = NamedId.Of(id1, "name2"); + + Assert.Equal(named_id1_name1_a, named_id1_name1_b); + Assert.Equal(named_id1_name1_a.GetHashCode(), named_id1_name1_b.GetHashCode()); + Assert.True(named_id1_name1_a.Equals((object)named_id1_name1_b)); + + Assert.NotEqual(named_id1_name1_a, named_id2_name1); + Assert.NotEqual(named_id1_name1_a.GetHashCode(), named_id2_name1.GetHashCode()); + Assert.False(named_id1_name1_a.Equals((object)named_id2_name1)); + + Assert.NotEqual(named_id1_name1_a, named_id1_name2); + Assert.NotEqual(named_id1_name1_a.GetHashCode(), named_id1_name2.GetHashCode()); + Assert.False(named_id1_name1_a.Equals((object)named_id1_name2)); + } + + [Fact] + public void Should_serialize_and_deserialize_null_guid_token() + { + NamedId? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_valid_guid_token() + { + var value = NamedId.Of(Guid.NewGuid(), "my-name"); + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_null_long_token() + { + NamedId? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_valid_long_token() + { + var value = NamedId.Of(123L, "my-name"); + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_null_string_token() + { + NamedId? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_valid_string_token() + { + var value = NamedId.Of(Guid.NewGuid().ToString(), "my-name"); + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_throw_exception_if_string_id_is_not_valid() + { + Assert.ThrowsAny(() => JsonHelper.Deserialize>("123")); + } + + [Fact] + public void Should_throw_exception_if_long_id_is_not_valid() + { + Assert.ThrowsAny(() => JsonHelper.Deserialize>("invalid-long,name")); + } + + [Fact] + public void Should_throw_exception_if_guid_id_is_not_valid() + { + Assert.ThrowsAny(() => JsonHelper.Deserialize>("invalid-guid,name")); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Net/IPAddressComparerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Net/IPAddressComparerTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Net/IPAddressComparerTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Net/IPAddressComparerTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs new file mode 100644 index 000000000..16c4baa0c --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs @@ -0,0 +1,197 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Xunit; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public class UniqueNameIndexGrainTests + { + private readonly IGrainState> grainState = A.Fake>>(); + private readonly NamedId id1 = NamedId.Of(Guid.NewGuid(), "my-name1"); + private readonly NamedId id2 = NamedId.Of(Guid.NewGuid(), "my-name2"); + private readonly UniqueNameIndexGrain, Guid> sut; + + public UniqueNameIndexGrainTests() + { + A.CallTo(() => grainState.ClearAsync()) + .Invokes(() => grainState.Value = new UniqueNameIndexState()); + + sut = new UniqueNameIndexGrain, Guid>(grainState); + } + + [Fact] + public async Task Should_not_write_to_state_for_reservation() + { + await sut.ReserveAsync(id1.Id, id1.Name); + + A.CallTo(() => grainState.WriteAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_add_to_index_if_reservation_token_acquired() + { + await AddAsync(id1); + + var result = await sut.GetIdAsync(id1.Name); + + Assert.Equal(id1.Id, result); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_make_reservation_if_name_already_reserved() + { + await sut.ReserveAsync(id1.Id, id1.Name); + + var newToken = await sut.ReserveAsync(id1.Id, id1.Name); + + Assert.Null(newToken); + } + + [Fact] + public async Task Should_not_make_reservation_if_name_taken() + { + await AddAsync(id1); + + var newToken = await sut.ReserveAsync(id1.Id, id1.Name); + + Assert.Null(newToken); + } + + [Fact] + public async Task Should_provide_number_of_entries() + { + await AddAsync(id1); + await AddAsync(id2); + + var count = await sut.CountAsync(); + + Assert.Equal(2, count); + } + + [Fact] + public async Task Should_clear_all_entries() + { + await AddAsync(id1); + await AddAsync(id2); + + await sut.ClearAsync(); + + var count = await sut.CountAsync(); + + Assert.Equal(0, count); + } + + [Fact] + public async Task Should_make_reservation_after_reservation_removed() + { + var token = await sut.ReserveAsync(id1.Id, id1.Name); + + await sut.RemoveReservationAsync(token!); + + var newToken = await sut.ReserveAsync(id1.Id, id1.Name); + + Assert.NotNull(newToken); + } + + [Fact] + public async Task Should_make_reservation_after_id_removed() + { + await AddAsync(id1); + + await sut.RemoveAsync(id1.Id); + + var newToken = await sut.ReserveAsync(id1.Id, id1.Name); + + Assert.NotNull(newToken); + } + + [Fact] + public async Task Should_remove_id_from_index() + { + await AddAsync(id1); + + await sut.RemoveAsync(id1.Id); + + var result = await sut.GetIdAsync(id1.Name); + + Assert.Equal(Guid.Empty, result); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task Should_not_write_to_state_if_nothing_removed() + { + await sut.RemoveAsync(id1.Id); + + A.CallTo(() => grainState.WriteAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_ignore_error_if_removing_reservation_with_Invalid_token() + { + await sut.RemoveReservationAsync(null); + } + + [Fact] + public async Task Should_ignore_error_if_completing_reservation_with_Invalid_token() + { + await sut.AddAsync(null!); + } + + [Fact] + public async Task Should_replace_ids_on_rebuild() + { + var state = new Dictionary + { + [id1.Name] = id1.Id, + [id2.Name] = id2.Id + }; + + await sut.RebuildAsync(state); + + Assert.Equal(id1.Id, await sut.GetIdAsync(id1.Name)); + Assert.Equal(id2.Id, await sut.GetIdAsync(id2.Name)); + + var result = await sut.GetIdsAsync(); + + Assert.Equal(new List { id1.Id, id2.Id }, result); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_provide_multiple_ids_by_names() + { + await AddAsync(id1); + await AddAsync(id2); + + var result = await sut.GetIdsAsync(new string[] { id1.Name, id2.Name, "not-found" }); + + Assert.Equal(new List { id1.Id, id2.Id }, result); + } + + private async Task AddAsync(NamedId id) + { + var token = await sut.ReserveAsync(id.Id, id.Name); + + await sut.AddAsync(token!); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs new file mode 100644 index 000000000..18058ebd8 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs @@ -0,0 +1,119 @@ +// ========================================================================== +// JsonExternalSerializerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.IO; +using FakeItEasy; +using Orleans.Serialization; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Orleans +{ + public class JsonExternalSerializerTests + { + public JsonExternalSerializerTests() + { + J.DefaultSerializer = JsonHelper.DefaultSerializer; + } + + [Fact] + public void Should_not_copy_null() + { + var v = (string?)null; + var c = J.Copy(v, null); + + Assert.Null(c); + } + + [Fact] + public void Should_copy_null_json() + { + var v = new J?>(null); + var c = (J>)J.Copy(v, null)!; + + Assert.Null(c.Value); + } + + [Fact] + public void Should_not_copy_immutable_values() + { + var v = new List { 1, 2, 3 }.AsJ(); + var c = (J>)J.Copy(v, null)!; + + Assert.Same(v.Value, c.Value); + } + + [Fact] + public void Should_serialize_and_deserialize_value() + { + SerializeAndDeserialize(ArrayOfLength(100), Assert.Equal); + } + + [Fact] + public void Should_serialize_and_deserialize_large_value() + { + SerializeAndDeserialize(ArrayOfLength(8000), Assert.Equal); + } + + private static void SerializeAndDeserialize(T value, Action equals) where T : class + { + using (var buffer = new MemoryStream()) + { + J.Serialize(J.Of(value), CreateWriter(buffer), typeof(T)); + + buffer.Position = 0; + + var copy = (J)J.Deserialize(typeof(J), CreateReader(buffer))!; + + equals(copy.Value, value); + + Assert.NotSame(value, copy.Value); + } + } + + private static DeserializationContext CreateReader(MemoryStream buffer) + { + var reader = A.Fake(); + + A.CallTo(() => reader.ReadByteArray(A.Ignored, A.Ignored, A.Ignored)) + .Invokes(new Action((b, o, l) => buffer.Read(b, o, l))); + A.CallTo(() => reader.CurrentPosition) + .ReturnsLazily(x => (int)buffer.Position); + A.CallTo(() => reader.Length) + .ReturnsLazily(x => (int)buffer.Length); + + return new DeserializationContext(null) { StreamReader = reader }; + } + + private static SerializationContext CreateWriter(MemoryStream buffer) + { + var writer = A.Fake(); + + A.CallTo(() => writer.Write(A.Ignored, A.Ignored, A.Ignored)) + .Invokes(new Action(buffer.Write)); + A.CallTo(() => writer.CurrentOffset) + .ReturnsLazily(x => (int)buffer.Position); + + return new SerializationContext(null) { StreamWriter = writer }; + } + + private static List ArrayOfLength(int length) + { + var result = new List(); + + for (var i = 0; i < length; i++) + { + result.Add(i); + } + + return result; + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/LockGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/LockGrainTests.cs new file mode 100644 index 000000000..2b101774c --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Orleans/LockGrainTests.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.Orleans +{ + public class LockGrainTests + { + private readonly LockGrain sut = new LockGrain(); + + [Fact] + public async Task Should_not_acquire_lock_when_locked() + { + var releaseLock1 = await sut.AcquireLockAsync("Key1"); + var releaseLock2 = await sut.AcquireLockAsync("Key1"); + + Assert.NotNull(releaseLock1); + Assert.Null(releaseLock2); + } + + [Fact] + public async Task Should_acquire_lock_with_other_key() + { + var releaseLock1 = await sut.AcquireLockAsync("Key1"); + var releaseLock2 = await sut.AcquireLockAsync("Key2"); + + Assert.NotNull(releaseLock1); + Assert.NotNull(releaseLock2); + } + + [Fact] + public async Task Should_acquire_lock_after_released() + { + var releaseLock1 = await sut.AcquireLockAsync("Key1"); + + await sut.ReleaseLockAsync(releaseLock1!); + + var releaseLock2 = await sut.AcquireLockAsync("Key1"); + + Assert.NotNull(releaseLock1); + Assert.NotNull(releaseLock2); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs new file mode 100644 index 000000000..0d7dc6ad3 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs @@ -0,0 +1,382 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using NJsonSchema; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.TestHelpers; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class JsonQueryConversionTests + { + private readonly List errors = new List(); + private readonly JsonSchema schema = new JsonSchema(); + + public JsonQueryConversionTests() + { + var nested = new JsonSchemaProperty { Title = "nested" }; + + nested.Properties["property"] = new JsonSchemaProperty + { + Type = JsonObjectType.String + }; + + schema.Properties["boolean"] = new JsonSchemaProperty + { + Type = JsonObjectType.Boolean + }; + + schema.Properties["datetime"] = new JsonSchemaProperty + { + Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime + }; + + schema.Properties["guid"] = new JsonSchemaProperty + { + Type = JsonObjectType.String, Format = JsonFormatStrings.Guid + }; + + schema.Properties["integer"] = new JsonSchemaProperty + { + Type = JsonObjectType.Integer + }; + + schema.Properties["number"] = new JsonSchemaProperty + { + Type = JsonObjectType.Number + }; + + schema.Properties["string"] = new JsonSchemaProperty + { + Type = JsonObjectType.String + }; + + schema.Properties["stringArray"] = new JsonSchemaProperty + { + Item = new JsonSchema + { + Type = JsonObjectType.String + }, + Type = JsonObjectType.Array + }; + + schema.Properties["object"] = nested; + + schema.Properties["reference"] = new JsonSchemaProperty + { + Reference = nested + }; + } + + [Fact] + public void Should_add_error_if_property_does_not_exist() + { + var json = new { path = "notfound", op = "eq", value = 1 }; + + AssertErrors(json, "Path 'notfound' does not point to a valid property in the model."); + } + + [Fact] + public void Should_add_error_if_nested_property_does_not_exist() + { + var json = new { path = "object.notfound", op = "eq", value = 1 }; + + AssertErrors(json, "'notfound' is not a property of 'nested'."); + } + + [Theory] + [InlineData("contains", "contains(datetime, 2012-11-10T09:08:07Z)")] + [InlineData("empty", "empty(datetime)")] + [InlineData("endswith", "endsWith(datetime, 2012-11-10T09:08:07Z)")] + [InlineData("eq", "datetime == 2012-11-10T09:08:07Z")] + [InlineData("ge", "datetime >= 2012-11-10T09:08:07Z")] + [InlineData("gt", "datetime > 2012-11-10T09:08:07Z")] + [InlineData("le", "datetime <= 2012-11-10T09:08:07Z")] + [InlineData("lt", "datetime < 2012-11-10T09:08:07Z")] + [InlineData("ne", "datetime != 2012-11-10T09:08:07Z")] + [InlineData("startswith", "startsWith(datetime, 2012-11-10T09:08:07Z)")] + public void Should_parse_datetime_string_filter(string op, string expected) + { + var json = new { path = "datetime", op, value = "2012-11-10T09:08:07Z" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_parse_date_string_filter() + { + var json = new { path = "datetime", op = "eq", value = "2012-11-10" }; + + AssertFilter(json, "datetime == 2012-11-10T00:00:00Z"); + } + + [Fact] + public void Should_add_error_if_datetime_string_property_got_invalid_string_value() + { + var json = new { path = "datetime", op = "eq", value = "invalid" }; + + AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got invalid String."); + } + + [Fact] + public void Should_add_error_if_datetime_string_property_got_invalid_value() + { + var json = new { path = "datetime", op = "eq", value = 1 }; + + AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got Number."); + } + + [Theory] + [InlineData("contains", "contains(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] + [InlineData("empty", "empty(guid)")] + [InlineData("endswith", "endsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] + [InlineData("eq", "guid == bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("ge", "guid >= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("gt", "guid > bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("le", "guid <= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("lt", "guid < bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("ne", "guid != bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("startswith", "startsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] + public void Should_parse_guid_string_filter(string op, string expected) + { + var json = new { path = "guid", op, value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_guid_string_property_got_invalid_string_value() + { + var json = new { path = "guid", op = "eq", value = "invalid" }; + + AssertErrors(json, "Expected Guid String for path 'guid', but got invalid String."); + } + + [Fact] + public void Should_add_error_if_guid_string_property_got_invalid_value() + { + var json = new { path = "guid", op = "eq", value = 1 }; + + AssertErrors(json, "Expected Guid String for path 'guid', but got Number."); + } + + [Theory] + [InlineData("contains", "contains(string, 'Hello')")] + [InlineData("empty", "empty(string)")] + [InlineData("endswith", "endsWith(string, 'Hello')")] + [InlineData("eq", "string == 'Hello'")] + [InlineData("ge", "string >= 'Hello'")] + [InlineData("gt", "string > 'Hello'")] + [InlineData("le", "string <= 'Hello'")] + [InlineData("lt", "string < 'Hello'")] + [InlineData("ne", "string != 'Hello'")] + [InlineData("startswith", "startsWith(string, 'Hello')")] + public void Should_parse_string_filter(string op, string expected) + { + var json = new { path = "string", op, value = "Hello" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_string_property_got_invalid_value() + { + var json = new { path = "string", op = "eq", value = 1 }; + + AssertErrors(json, "Expected String for path 'string', but got Number."); + } + + [Fact] + public void Should_parse_string_in_filter() + { + var json = new { path = "string", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "string in ['Hello']"); + } + + [Fact] + public void Should_parse_nested_string_filter() + { + var json = new { path = "object.property", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "object.property in ['Hello']"); + } + + [Fact] + public void Should_parse_referenced_string_filter() + { + var json = new { path = "reference.property", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "reference.property in ['Hello']"); + } + + [Theory] + [InlineData("eq", "number == 12")] + [InlineData("ge", "number >= 12")] + [InlineData("gt", "number > 12")] + [InlineData("le", "number <= 12")] + [InlineData("lt", "number < 12")] + [InlineData("ne", "number != 12")] + public void Should_parse_number_filter(string op, string expected) + { + var json = new { path = "number", op, value = 12 }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_number_property_got_invalid_value() + { + var json = new { path = "number", op = "eq", value = true }; + + AssertErrors(json, "Expected Number for path 'number', but got Boolean."); + } + + [Fact] + public void Should_parse_number_in_filter() + { + var json = new { path = "number", op = "in", value = new[] { 12 } }; + + AssertFilter(json, "number in [12]"); + } + + [Theory] + [InlineData("eq", "boolean == True")] + [InlineData("ne", "boolean != True")] + public void Should_parse_boolean_filter(string op, string expected) + { + var json = new { path = "boolean", op, value = true }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_boolean_property_got_invalid_value() + { + var json = new { path = "boolean", op = "eq", value = 1 }; + + AssertErrors(json, "Expected Boolean for path 'boolean', but got Number."); + } + + [Fact] + public void Should_parse_boolean_in_filter() + { + var json = new { path = "boolean", op = "in", value = new[] { true } }; + + AssertFilter(json, "boolean in [True]"); + } + + [Theory] + [InlineData("empty", "empty(stringArray)")] + [InlineData("eq", "stringArray == 'Hello'")] + [InlineData("ne", "stringArray != 'Hello'")] + public void Should_parse_array_filter(string op, string expected) + { + var json = new { path = "stringArray", op, value = "Hello" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_parse_array_in_filter() + { + var json = new { path = "stringArray", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "stringArray in ['Hello']"); + } + + [Fact] + public void Should_add_error_when_using_array_value_for_non_allowed_operator() + { + var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; + + AssertErrors(json, "Array value is not allowed for 'Equals' operator and path 'string'."); + } + + [Fact] + public void Should_parse_query() + { + var json = new { skip = 10, take = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; + + AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); + } + + [Fact] + public void Should_parse_query_with_sorting() + { + var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; + + AssertQuery(json, "Sort: string Ascending"); + } + + [Fact] + public void Should_throw_exception_for_invalid_query() + { + var json = new { sort = new[] { new { path = "invalid", order = "ascending" } } }; + + Assert.Throws(() => AssertQuery(json, null)); + } + + [Fact] + public void Should_throw_exception_when_parsing_invalid_json() + { + var json = "invalid"; + + Assert.Throws(() => AssertQuery(json, null)); + } + + private void AssertQuery(object json, string? expectedFilter) + { + var filter = ConvertQuery(json); + + Assert.Empty(errors); + + Assert.Equal(expectedFilter, filter); + } + + private void AssertFilter(object json, string? expectedFilter) + { + var filter = ConvertFilter(json); + + Assert.Empty(errors); + + Assert.Equal(expectedFilter, filter); + } + + private void AssertErrors(object json, params string[] expectedErrors) + { + var filter = ConvertFilter(json); + + Assert.Equal(expectedErrors.ToList(), errors); + + Assert.Null(filter); + } + + private string? ConvertFilter(T value) + { + var json = JsonHelper.DefaultSerializer.Serialize(value, true); + + var jsonFilter = JsonHelper.DefaultSerializer.Deserialize>(json); + + return JsonFilterVisitor.Parse(jsonFilter, schema, errors)?.ToString(); + } + + private string? ConvertQuery(T value) + { + var json = JsonHelper.DefaultSerializer.Serialize(value, true); + + var jsonFilter = schema.Parse(json, JsonHelper.DefaultSerializer); + + return jsonFilter.ToString(); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs new file mode 100644 index 000000000..afe253aba --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Xunit; + +namespace Squidex.Infrastructure.Queries +{ + public class PascalCasePathConverterTests + { + [Fact] + public void Should_convert_property() + { + var source = ClrFilter.Eq("property", 1); + var result = PascalCasePathConverter.Transform(source); + + Assert.Equal("Property == 1", result!.ToString()); + } + + [Fact] + public void Should_convert_properties() + { + var source = ClrFilter.Eq("root.child", 1); + var result = PascalCasePathConverter.Transform(source); + + Assert.Equal("Root.Child == 1", result!.ToString()); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs new file mode 100644 index 000000000..be114009d --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs @@ -0,0 +1,374 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using NJsonSchema; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.TestHelpers; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class QueryJsonConversionTests + { + private readonly List errors = new List(); + private readonly JsonSchema schema = new JsonSchema(); + + public QueryJsonConversionTests() + { + var nested = new JsonSchemaProperty { Title = "nested" }; + + nested.Properties["property"] = new JsonSchemaProperty + { + Type = JsonObjectType.String + }; + + schema.Properties["boolean"] = new JsonSchemaProperty + { + Type = JsonObjectType.Boolean + }; + + schema.Properties["datetime"] = new JsonSchemaProperty + { + Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime + }; + + schema.Properties["guid"] = new JsonSchemaProperty + { + Type = JsonObjectType.String, Format = JsonFormatStrings.Guid + }; + + schema.Properties["integer"] = new JsonSchemaProperty + { + Type = JsonObjectType.Integer + }; + + schema.Properties["number"] = new JsonSchemaProperty + { + Type = JsonObjectType.Number + }; + + schema.Properties["string"] = new JsonSchemaProperty + { + Type = JsonObjectType.String + }; + + schema.Properties["stringArray"] = new JsonSchemaProperty + { + Item = new JsonSchema + { + Type = JsonObjectType.String + }, + Type = JsonObjectType.Array + }; + + schema.Properties["object"] = nested; + + schema.Properties["reference"] = new JsonSchemaProperty + { + Reference = nested + }; + } + + [Fact] + public void Should_add_error_if_property_does_not_exist() + { + var json = new { path = "notfound", op = "eq", value = 1 }; + + AssertErrors(json, "Path 'notfound' does not point to a valid property in the model."); + } + + [Fact] + public void Should_add_error_if_nested_property_does_not_exist() + { + var json = new { path = "object.notfound", op = "eq", value = 1 }; + + AssertErrors(json, "'notfound' is not a property of 'nested'."); + } + + [Theory] + [InlineData("contains", "contains(datetime, 2012-11-10T09:08:07Z)")] + [InlineData("empty", "empty(datetime)")] + [InlineData("endswith", "endsWith(datetime, 2012-11-10T09:08:07Z)")] + [InlineData("eq", "datetime == 2012-11-10T09:08:07Z")] + [InlineData("ge", "datetime >= 2012-11-10T09:08:07Z")] + [InlineData("gt", "datetime > 2012-11-10T09:08:07Z")] + [InlineData("le", "datetime <= 2012-11-10T09:08:07Z")] + [InlineData("lt", "datetime < 2012-11-10T09:08:07Z")] + [InlineData("ne", "datetime != 2012-11-10T09:08:07Z")] + [InlineData("startswith", "startsWith(datetime, 2012-11-10T09:08:07Z)")] + public void Should_parse_datetime_string_filter(string op, string expected) + { + var json = new { path = "datetime", op, value = "2012-11-10T09:08:07Z" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_datetime_string_property_got_invalid_string_value() + { + var json = new { path = "datetime", op = "eq", value = "invalid" }; + + AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got invalid String."); + } + + [Fact] + public void Should_add_error_if_datetime_string_property_got_invalid_value() + { + var json = new { path = "datetime", op = "eq", value = 1 }; + + AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got Number."); + } + + [Theory] + [InlineData("contains", "contains(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] + [InlineData("empty", "empty(guid)")] + [InlineData("endswith", "endsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] + [InlineData("eq", "guid == bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("ge", "guid >= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("gt", "guid > bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("le", "guid <= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("lt", "guid < bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("ne", "guid != bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("startswith", "startsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] + public void Should_parse_guid_string_filter(string op, string expected) + { + var json = new { path = "guid", op, value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_guid_string_property_got_invalid_string_value() + { + var json = new { path = "guid", op = "eq", value = "invalid" }; + + AssertErrors(json, "Expected Guid String for path 'guid', but got invalid String."); + } + + [Fact] + public void Should_add_error_if_guid_string_property_got_invalid_value() + { + var json = new { path = "guid", op = "eq", value = 1 }; + + AssertErrors(json, "Expected Guid String for path 'guid', but got Number."); + } + + [Theory] + [InlineData("contains", "contains(string, 'Hello')")] + [InlineData("empty", "empty(string)")] + [InlineData("endswith", "endsWith(string, 'Hello')")] + [InlineData("eq", "string == 'Hello'")] + [InlineData("ge", "string >= 'Hello'")] + [InlineData("gt", "string > 'Hello'")] + [InlineData("le", "string <= 'Hello'")] + [InlineData("lt", "string < 'Hello'")] + [InlineData("ne", "string != 'Hello'")] + [InlineData("startswith", "startsWith(string, 'Hello')")] + public void Should_parse_string_filter(string op, string expected) + { + var json = new { path = "string", op, value = "Hello" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_string_property_got_invalid_value() + { + var json = new { path = "string", op = "eq", value = 1 }; + + AssertErrors(json, "Expected String for path 'string', but got Number."); + } + + [Fact] + public void Should_parse_string_in_filter() + { + var json = new { path = "string", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "string in ['Hello']"); + } + + [Fact] + public void Should_parse_nested_string_filter() + { + var json = new { path = "object.property", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "object.property in ['Hello']"); + } + + [Fact] + public void Should_parse_referenced_string_filter() + { + var json = new { path = "reference.property", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "reference.property in ['Hello']"); + } + + [Theory] + [InlineData("eq", "number == 12")] + [InlineData("ge", "number >= 12")] + [InlineData("gt", "number > 12")] + [InlineData("le", "number <= 12")] + [InlineData("lt", "number < 12")] + [InlineData("ne", "number != 12")] + public void Should_parse_number_filter(string op, string expected) + { + var json = new { path = "number", op, value = 12 }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_number_property_got_invalid_value() + { + var json = new { path = "number", op = "eq", value = true }; + + AssertErrors(json, "Expected Number for path 'number', but got Boolean."); + } + + [Fact] + public void Should_parse_number_in_filter() + { + var json = new { path = "number", op = "in", value = new[] { 12 } }; + + AssertFilter(json, "number in [12]"); + } + + [Theory] + [InlineData("eq", "boolean == True")] + [InlineData("ne", "boolean != True")] + public void Should_parse_boolean_filter(string op, string expected) + { + var json = new { path = "boolean", op, value = true }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_boolean_property_got_invalid_value() + { + var json = new { path = "boolean", op = "eq", value = 1 }; + + AssertErrors(json, "Expected Boolean for path 'boolean', but got Number."); + } + + [Fact] + public void Should_parse_boolean_in_filter() + { + var json = new { path = "boolean", op = "in", value = new[] { true } }; + + AssertFilter(json, "boolean in [True]"); + } + + [Theory] + [InlineData("empty", "empty(stringArray)")] + [InlineData("eq", "stringArray == 'Hello'")] + [InlineData("ne", "stringArray != 'Hello'")] + public void Should_parse_array_filter(string op, string expected) + { + var json = new { path = "stringArray", op, value = "Hello" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_parse_array_in_filter() + { + var json = new { path = "stringArray", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "stringArray in ['Hello']"); + } + + [Fact] + public void Should_add_error_when_using_array_value_for_non_allowed_operator() + { + var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; + + AssertErrors(json, "Array value is not allowed for 'Equals' operator and path 'string'."); + } + + [Fact] + public void Should_parse_query() + { + var json = new { skip = 10, take = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; + + AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); + } + + [Fact] + public void Should_parse_query_with_sorting() + { + var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; + + AssertQuery(json, "Sort: string Ascending"); + } + + [Fact] + public void Should_throw_exception_for_invalid_query() + { + var json = new { sort = new[] { new { path = "invalid", order = "ascending" } } }; + + Assert.Throws(() => AssertQuery(json, null)); + } + + [Fact] + public void Should_throw_exception_when_parsing_invalid_json() + { + var json = "invalid"; + + Assert.Throws(() => AssertQuery(json, null)); + } + + private void AssertQuery(object json, string? expectedFilter) + { + var filter = ConvertQuery(json); + + Assert.Empty(errors); + + Assert.Equal(expectedFilter, filter); + } + + private void AssertFilter(object json, string? expectedFilter) + { + var filter = ConvertFilter(json); + + Assert.Empty(errors); + + Assert.Equal(expectedFilter, filter); + } + + private void AssertErrors(object json, params string[] expectedErrors) + { + var filter = ConvertFilter(json); + + Assert.Equal(expectedErrors.ToList(), errors); + + Assert.Null(filter); + } + + private string? ConvertFilter(T value) + { + var json = JsonHelper.DefaultSerializer.Serialize(value, true); + + var jsonFilter = JsonHelper.DefaultSerializer.Deserialize>(json); + + return JsonFilterVisitor.Parse(jsonFilter, schema, errors)?.ToString(); + } + + private string? ConvertQuery(T value) + { + var json = JsonHelper.DefaultSerializer.Serialize(value, true); + + var jsonFilter = schema.Parse(json, JsonHelper.DefaultSerializer); + + return jsonFilter.ToString(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs new file mode 100644 index 000000000..4347c3523 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs @@ -0,0 +1,424 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.OData.Edm; +using Squidex.Infrastructure.Queries.OData; +using Xunit; + +namespace Squidex.Infrastructure.Queries +{ + public class QueryODataConversionTests + { + private static readonly IEdmModel EdmModel; + + static QueryODataConversionTests() + { + var entityType = new EdmEntityType("Squidex", "Users"); + + entityType.AddStructuralProperty("id", EdmPrimitiveTypeKind.Guid); + entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty("isComicFigure", EdmPrimitiveTypeKind.Boolean); + entityType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty("birthday", EdmPrimitiveTypeKind.Date); + entityType.AddStructuralProperty("incomeCents", EdmPrimitiveTypeKind.Int64); + entityType.AddStructuralProperty("incomeMio", EdmPrimitiveTypeKind.Double); + entityType.AddStructuralProperty("age", EdmPrimitiveTypeKind.Int32); + + var container = new EdmEntityContainer("Squidex", "Container"); + + container.AddEntitySet("UserSet", entityType); + + var model = new EdmModel(); + + model.AddElement(container); + model.AddElement(entityType); + + EdmModel = model; + } + + [Fact] + public void Should_parse_query() + { + var parser = EdmModel.ParseQuery("$filter=firstName eq 'Dagobert'"); + + Assert.NotNull(parser); + } + + [Fact] + public void Should_parse_filter_when_type_is_datetime() + { + var i = Q("$filter=created eq 1988-01-19T12:00:00Z"); + var o = C("Filter: created == 1988-01-19T12:00:00Z"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_datetime_list() + { + var i = Q("$filter=created in ('1988-01-19T12:00:00Z')"); + var o = C("Filter: created in [1988-01-19T12:00:00Z]"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_date() + { + var i = Q("$filter=created eq 1988-01-19"); + var o = C("Filter: created == 1988-01-19T00:00:00Z"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_date_list() + { + var i = Q("$filter=created in ('1988-01-19')"); + var o = C("Filter: created in [1988-01-19T00:00:00Z]"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_guid() + { + var i = Q("$filter=id eq B5FE25E3-B262-4B17-91EF-B3772A6B62BB"); + var o = C("Filter: id == b5fe25e3-b262-4b17-91ef-b3772a6b62bb"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_guid_list() + { + var i = Q("$filter=id in ('B5FE25E3-B262-4B17-91EF-B3772A6B62BB')"); + var o = C("Filter: id in [b5fe25e3-b262-4b17-91ef-b3772a6b62bb]"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_null() + { + var i = Q("$filter=firstName eq null"); + var o = C("Filter: firstName == null"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_string() + { + var i = Q("$filter=firstName eq 'Dagobert'"); + var o = C("Filter: firstName == 'Dagobert'"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_string_list() + { + var i = Q("$filter=firstName in ('Dagobert')"); + var o = C("Filter: firstName in ['Dagobert']"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_boolean() + { + var i = Q("$filter=isComicFigure eq true"); + var o = C("Filter: isComicFigure == True"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_boolean_list() + { + var i = Q("$filter=isComicFigure in (true)"); + var o = C("Filter: isComicFigure in [True]"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_int32() + { + var i = Q("$filter=age eq 60"); + var o = C("Filter: age == 60"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_int32_list() + { + var i = Q("$filter=age in (60)"); + var o = C("Filter: age in [60]"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_int64() + { + var i = Q("$filter=incomeCents eq 31543143513456789"); + var o = C("Filter: incomeCents == 31543143513456789"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_int64_list() + { + var i = Q("$filter=incomeCents in (31543143513456789)"); + var o = C("Filter: incomeCents in [31543143513456789]"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_double() + { + var i = Q("$filter=incomeMio eq 5634474356.1233"); + var o = C("Filter: incomeMio == 5634474356.1233"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_double_list() + { + var i = Q("$filter=incomeMio in (5634474356.1233)"); + var o = C("Filter: incomeMio in [5634474356.1233]"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_negation() + { + var i = Q("$filter=not endswith(lastName, 'Duck')"); + var o = C("Filter: !(endsWith(lastName, 'Duck'))"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_startswith() + { + var i = Q("$filter=startswith(lastName, 'Duck')"); + var o = C("Filter: startsWith(lastName, 'Duck')"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_endswith() + { + var i = Q("$filter=endswith(lastName, 'Duck')"); + var o = C("Filter: endsWith(lastName, 'Duck')"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_empty() + { + var i = Q("$filter=empty(lastName)"); + var o = C("Filter: empty(lastName)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_empty_to_true() + { + var i = Q("$filter=empty(lastName) eq true"); + var o = C("Filter: empty(lastName)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_contains() + { + var i = Q("$filter=contains(lastName, 'Duck')"); + var o = C("Filter: contains(lastName, 'Duck')"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_contains_to_true() + { + var i = Q("$filter=contains(lastName, 'Duck') eq true"); + var o = C("Filter: contains(lastName, 'Duck')"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_contains_to_false() + { + var i = Q("$filter=contains(lastName, 'Duck') eq false"); + var o = C("Filter: !(contains(lastName, 'Duck'))"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_equals() + { + var i = Q("$filter=age eq 1"); + var o = C("Filter: age == 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_notequals() + { + var i = Q("$filter=age ne 1"); + var o = C("Filter: age != 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_lessthan() + { + var i = Q("$filter=age lt 1"); + var o = C("Filter: age < 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_lessthanorequal() + { + var i = Q("$filter=age le 1"); + var o = C("Filter: age <= 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_greaterthan() + { + var i = Q("$filter=age gt 1"); + var o = C("Filter: age > 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_greaterthanorequal() + { + var i = Q("$filter=age ge 1"); + var o = C("Filter: age >= 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_conjunction_and_contains() + { + var i = Q("$filter=contains(firstName, 'Sebastian') eq false and isComicFigure eq true"); + var o = C("Filter: (!(contains(firstName, 'Sebastian')) && isComicFigure == True)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_conjunction() + { + var i = Q("$filter=age eq 1 and age eq 2"); + var o = C("Filter: (age == 1 && age == 2)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_disjunction() + { + var i = Q("$filter=age eq 1 or age eq 2"); + var o = C("Filter: (age == 1 || age == 2)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_full_text_numbers() + { + var i = Q("$search=\"33k\""); + var o = C("FullText: '33k'"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_full_text() + { + var i = Q("$search=Duck"); + var o = C("FullText: 'Duck'"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_full_text_and_multiple_terms() + { + var i = Q("$search=Dagobert or Donald"); + var o = C("FullText: 'Dagobert or Donald'"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_orderby_with_single_field() + { + var i = Q("$orderby=age desc"); + var o = C("Sort: age Descending"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_orderby_with_multiple_field() + { + var i = Q("$orderby=age, incomeMio desc"); + var o = C("Sort: age Ascending, incomeMio Descending"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_and_take() + { + var i = Q("$top=3&$skip=4"); + var o = C("Skip: 4; Take: 3"); + + Assert.Equal(o, i); + } + + private static string C(string value) + { + return value; + } + + private static string? Q(string value) + { + var parser = EdmModel.ParseQuery(value); + + return parser?.ToQuery().ToString(); + } + } +} \ No newline at end of file diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs new file mode 100644 index 000000000..47a5ba1f5 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs @@ -0,0 +1,94 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Xunit; + +namespace Squidex.Infrastructure.Queries +{ + public class QueryOptimizationTests + { + [Fact] + public void Should_not_convert_optimize_valid_logical_filter() + { + var source = ClrFilter.Or(ClrFilter.Eq("path", 2), ClrFilter.Eq("path", 3)); + + var result = Optimizer.Optimize(source); + + Assert.Equal("(path == 2 || path == 3)", result!.ToString()); + } + + [Fact] + public void Should_return_filter_When_logical_filter_has_one_child() + { + var source = ClrFilter.And(ClrFilter.Eq("path", 1), ClrFilter.Or()); + + var result = Optimizer.Optimize(source); + + Assert.Equal("path == 1", result!.ToString()); + } + + [Fact] + public void Should_return_null_when_filters_of_logical_filter_get_optimized_away() + { + var source = ClrFilter.And(ClrFilter.And()); + + var result = Optimizer.Optimize(source); + + Assert.Null(result); + } + + [Fact] + public void Should_return_null_when_logical_filter_has_no_filter() + { + var source = ClrFilter.And(); + + var result = Optimizer.Optimize(source); + + Assert.Null(result); + } + + [Fact] + public void Should_return_null_when_filter_of_negation_get_optimized_away() + { + var source = ClrFilter.Not(ClrFilter.And()); + + var result = Optimizer.Optimize(source); + + Assert.Null(result); + } + + [Fact] + public void Should_invert_equals_not_filter() + { + var source = ClrFilter.Not(ClrFilter.Eq("path", 1)); + + var result = Optimizer.Optimize(source); + + Assert.Equal("path != 1", result!.ToString()); + } + + [Fact] + public void Should_invert_notequals_not_filter() + { + var source = ClrFilter.Not(ClrFilter.Ne("path", 1)); + + var result = Optimizer.Optimize(source); + + Assert.Equal("path == 1", result!.ToString()); + } + + [Fact] + public void Should_not_convert_number_operator() + { + var source = ClrFilter.Not(ClrFilter.Lt("path", 1)); + + var result = Optimizer.Optimize(source); + + Assert.Equal("!(path < 1)", result!.ToString()); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/RandomHashTests.cs b/backend/tests/Squidex.Infrastructure.Tests/RandomHashTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/RandomHashTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/RandomHashTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs b/backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs new file mode 100644 index 000000000..9374b9845 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs @@ -0,0 +1,122 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class RefTokenTests + { + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(":")] + [InlineData("user")] + public void Should_throw_exception_if_parsing_invalid_input(string input) + { + Assert.Throws(() => RefToken.Parse(input)); + } + + [Fact] + public void Should_instantiate_token() + { + var token = new RefToken("client", "client1"); + + Assert.Equal("client", token.Type); + Assert.Equal("client1", token.Identifier); + + Assert.True(token.IsClient); + } + + [Fact] + public void Should_instantiate_subject_token() + { + var token = new RefToken("subject", "client1"); + + Assert.True(token.IsSubject); + } + + [Fact] + public void Should_instantiate_token_and_lower_type() + { + var token = new RefToken("Client", "client1"); + + Assert.Equal("client", token.Type); + Assert.Equal("client1", token.Identifier); + } + + [Fact] + public void Should_parse_user_token_from_string() + { + var token = RefToken.Parse("client:client1"); + + Assert.Equal("client", token.Type); + Assert.Equal("client1", token.Identifier); + } + + [Fact] + public void Should_parse_user_token_with_colon_in_identifier() + { + var token = RefToken.Parse("client:client1:app"); + + Assert.Equal("client", token.Type); + Assert.Equal("client1:app", token.Identifier); + } + + [Fact] + public void Should_convert_user_token_to_string() + { + var token = RefToken.Parse("client:client1"); + + Assert.Equal("client:client1", token.ToString()); + } + + [Fact] + public void Should_make_correct_equal_comparisons() + { + var token_type1_id1_a = RefToken.Parse("type1:client1"); + var token_type1_id1_b = RefToken.Parse("type1:client1"); + + var token_type2_id1 = RefToken.Parse("type2:client1"); + var token_type1_id2 = RefToken.Parse("type1:client2"); + + Assert.Equal(token_type1_id1_a, token_type1_id1_b); + Assert.Equal(token_type1_id1_a.GetHashCode(), token_type1_id1_b.GetHashCode()); + Assert.True(token_type1_id1_a.Equals((object)token_type1_id1_b)); + + Assert.NotEqual(token_type1_id1_a, token_type2_id1); + Assert.NotEqual(token_type1_id1_a.GetHashCode(), token_type2_id1.GetHashCode()); + Assert.False(token_type1_id1_a.Equals((object)token_type2_id1)); + + Assert.NotEqual(token_type1_id1_a, token_type1_id2); + Assert.NotEqual(token_type1_id1_a.GetHashCode(), token_type1_id2.GetHashCode()); + Assert.False(token_type1_id1_a.Equals((object)token_type1_id2)); + } + + [Fact] + public void Should_serialize_and_deserialize_null_token() + { + RefToken? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_valid_token() + { + var value = RefToken.Parse("client:client1"); + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs new file mode 100644 index 000000000..f951970db --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs @@ -0,0 +1,177 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics; +using Xunit; + +namespace Squidex.Infrastructure.Reflection +{ + public class SimpleMapperTests + { + public class Class1Base + { + public T1 P1 { get; set; } + } + + public class Class1 : Class1Base + { + public T2 P2 { get; set; } + } + + public class Class2Base + { + public T2 P2 { get; set; } + } + + public class Class2 : Class2Base + { + public T3 P3 { get; set; } + } + + public class Readonly + { + public T P1 { get; } + } + + public class Writeonly + { + public T P1 + { + set { Debug.WriteLine(value); } + } + } + + [Fact] + public void Should_throw_exception_if_mapping_with_null_source() + { + Assert.Throws(() => SimpleMapper.Map((Class2?)null!, new Class2())); + } + + [Fact] + public void Should_throw_exception_if_mapping_with_null_target() + { + Assert.Throws(() => SimpleMapper.Map(new Class2(), (Class2?)null!)); + } + + [Fact] + public void Should_map_to_same_type() + { + var obj1 = new Class1 + { + P1 = 6, + P2 = 8 + }; + var obj2 = SimpleMapper.Map(obj1, new Class2()); + + Assert.Equal(8, obj2.P2); + Assert.Equal(0, obj2.P3); + } + + [Fact] + public void Should_map_all_properties() + { + var obj1 = new Class1 + { + P1 = 6, + P2 = 8 + }; + var obj2 = SimpleMapper.Map(obj1, new Class1()); + + Assert.Equal(6, obj2.P1); + Assert.Equal(8, obj2.P2); + } + + [Fact] + public void Should_map_to_convertible_type() + { + var obj1 = new Class1 + { + P1 = 6, + P2 = 8 + }; + var obj2 = SimpleMapper.Map(obj1, new Class2()); + + Assert.Equal(8, obj2.P2); + Assert.Equal(0, obj2.P3); + } + + [Fact] + public void Should_map_nullables() + { + var obj1 = new Class1 + { + P1 = true, + P2 = true + }; + var obj2 = SimpleMapper.Map(obj1, new Class2()); + + Assert.True(obj2.P2); + Assert.False(obj2.P3); + } + + [Fact] + public void Should_map_when_convertible_is_null() + { + var obj1 = new Class1 + { + P1 = null, + P2 = null + }; + var obj2 = SimpleMapper.Map(obj1, new Class1()); + + Assert.Equal(0, obj2.P1); + Assert.Equal(0, obj2.P2); + } + + [Fact] + public void Should_convert_to_string() + { + var obj1 = new Class1 + { + P1 = new RefToken("user", "1"), + P2 = new RefToken("user", "2") + }; + var obj2 = SimpleMapper.Map(obj1, new Class2()); + + Assert.Equal("user:2", obj2.P2); + Assert.Null(obj2.P3); + } + + [Fact] + public void Should_return_default_if_conversion_failed() + { + var obj1 = new Class1 + { + P1 = long.MaxValue, + P2 = long.MaxValue + }; + var obj2 = SimpleMapper.Map(obj1, new Class2()); + + Assert.Equal(0, obj2.P2); + Assert.Equal(0, obj2.P3); + } + + [Fact] + public void Should_ignore_write_only() + { + var obj1 = new Writeonly(); + var obj2 = SimpleMapper.Map(obj1, new Class1()); + + Assert.Equal(0, obj2.P1); + } + + [Fact] + public void Should_ignore_read_only() + { + var obj1 = new Class1 { P1 = 10 }; + var obj2 = SimpleMapper.Map(obj1, new Readonly()); + + Assert.Equal(0, obj2.P1); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs b/backend/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs new file mode 100644 index 000000000..d41c32120 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Security.Claims; +using Xunit; + +namespace Squidex.Infrastructure.Security +{ + public class ExtensionsTests + { + [Fact] + public void Should_retrieve_subject() + { + TestClaimExtension(OpenIdClaims.Subject, x => x.OpenIdSubject()); + } + + [Fact] + public void Should_retrieve_client_id() + { + TestClaimExtension(OpenIdClaims.ClientId, x => x.OpenIdClientId()); + } + + [Fact] + public void Should_retrieve_preferred_user_name() + { + TestClaimExtension(OpenIdClaims.PreferredUserName, x => x.OpenIdPreferredUserName()); + } + + [Fact] + public void Should_retrieve_name() + { + TestClaimExtension(OpenIdClaims.Name, x => x.OpenIdName()); + } + + [Fact] + public void Should_retrieve_nickname() + { + TestClaimExtension(OpenIdClaims.NickName, x => x.OpenIdNickName()); + } + + [Fact] + public void Should_retrieve_email() + { + TestClaimExtension(OpenIdClaims.Email, x => x.OpenIdEmail()); + } + + private static void TestClaimExtension(string claimType, Func getter) + { + var claimValue = Guid.NewGuid().ToString(); + + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(); + + claimsIdentity.AddClaim(new Claim(claimType, claimValue)); + + Assert.Null(getter(claimsPrincipal)); + + claimsPrincipal.AddIdentity(claimsIdentity); + + Assert.Equal(claimValue, getter(claimsPrincipal)); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj new file mode 100644 index 000000000..9d268d3f0 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -0,0 +1,46 @@ + + + Exe + netcoreapp3.0 + Squidex.Infrastructure + 8.0 + enable + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + ..\..\Squidex.ruleset + + + + + + + + + \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/States/DefaultStreamNameResolverTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/DefaultStreamNameResolverTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/States/DefaultStreamNameResolverTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/States/DefaultStreamNameResolverTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs new file mode 100644 index 000000000..f91f6e3be --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.States +{ + public class InconsistentStateExceptionTests + { + [Fact] + public void Should_serialize_and_deserialize() + { + var source = new InconsistentStateException(100, 200, new InvalidOperationException("Inner")); + var result = source.SerializeAndDeserializeBinary(); + + Assert.IsType(result.InnerException); + + Assert.Equal("Inner", result.InnerException?.Message); + + Assert.Equal(result.ExpectedVersion, source.ExpectedVersion); + Assert.Equal(result.CurrentVersion, source.CurrentVersion); + + Assert.Equal(result.Message, source.Message); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockPoolTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockPoolTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockPoolTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockPoolTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs rename to backend/tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs new file mode 100644 index 000000000..d4180f81b --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Newtonsoft; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Infrastructure.TestHelpers +{ + public static class JsonHelper + { + public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); + + public static IJsonSerializer CreateSerializer(TypeNameRegistry? typeNameRegistry = null) + { + var serializerSettings = DefaultSettings(typeNameRegistry); + + return new NewtonsoftJsonSerializer(serializerSettings); + } + + public static JsonSerializerSettings DefaultSettings(TypeNameRegistry? typeNameRegistry = null) + { + return new JsonSerializerSettings + { + SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry ?? new TypeNameRegistry()), + + ContractResolver = new ConverterContractResolver( + new ClaimsPrincipalConverter(), + new InstantConverter(), + new EnvelopeHeadersConverter(), + new FilterConverter(), + new JsonValueConverter(), + new LanguageConverter(), + new NamedGuidIdConverter(), + new NamedLongIdConverter(), + new NamedStringIdConverter(), + new PropertyPathConverter(), + new RefTokenConverter(), + new StringEnumConverter()), + + TypeNameHandling = TypeNameHandling.Auto + }; + } + + public static T SerializeAndDeserialize(this T value) + { + return DefaultSerializer.Deserialize>(DefaultSerializer.Serialize(Tuple.Create(value))).Item1; + } + + public static T Deserialize(string value) + { + return DefaultSerializer.Deserialize>($"{{ \"Item1\": \"{value}\" }}").Item1; + } + + public static T Deserialize(object value) + { + return DefaultSerializer.Deserialize>($"{{ \"Item1\": {value} }}").Item1; + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs rename to backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs new file mode 100644 index 000000000..f8c1593f8 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.TestHelpers +{ + public sealed class MyDomainObject : DomainObjectGrain + { + public MyDomainObject(IStore store) + : base(store, A.Dummy()) + { + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + switch (command) + { + case CreateAuto createAuto: + return Create(createAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case CreateCustom createCustom: + return CreateReturn(createCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "CREATED"; + }); + + case UpdateAuto updateAuto: + return Update(updateAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case UpdateCustom updateCustom: + return UpdateReturn(updateCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "UPDATED"; + }); + } + + return Task.FromResult(null); + } + } + + public sealed class CreateAuto : MyCommand + { + public int Value { get; set; } + } + + public sealed class CreateCustom : MyCommand + { + public int Value { get; set; } + } + + public sealed class UpdateAuto : MyCommand + { + public int Value { get; set; } + } + + public sealed class UpdateCustom : MyCommand + { + public int Value { get; set; } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs rename to backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs rename to backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs new file mode 100644 index 000000000..56cefc7d7 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.TestHelpers +{ + public class MyGrain : DomainObjectGrain + { + public MyGrain(IStore store) + : base(store, A.Dummy()) + { + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + return Task.FromResult(null); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs b/backend/tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs new file mode 100644 index 000000000..61096410c --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs @@ -0,0 +1,228 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Squidex.Infrastructure.Log; +using Xunit; + +namespace Squidex.Infrastructure.UsageTracking +{ + public class BackgroundUsageTrackerTests + { + private readonly IUsageRepository usageStore = A.Fake(); + private readonly ISemanticLog log = A.Fake(); + private readonly string key = Guid.NewGuid().ToString(); + private readonly BackgroundUsageTracker sut; + + public BackgroundUsageTrackerTests() + { + sut = new BackgroundUsageTracker(usageStore, log); + } + + [Fact] + public async Task Should_throw_exception_if_tracking_on_disposed_object() + { + sut.Dispose(); + + await Assert.ThrowsAsync(() => sut.TrackAsync(key, "category1", 1, 1000)); + } + + [Fact] + public async Task Should_throw_exception_if_querying_on_disposed_object() + { + sut.Dispose(); + + await Assert.ThrowsAsync(() => sut.QueryAsync(key, DateTime.Today, DateTime.Today.AddDays(1))); + } + + [Fact] + public async Task Should_throw_exception_if_querying_montly_usage_on_disposed_object() + { + sut.Dispose(); + + await Assert.ThrowsAsync(() => sut.GetMonthlyCallsAsync(key, DateTime.Today)); + } + + [Fact] + public async Task Should_sum_up_when_getting_monthly_calls() + { + var date = new DateTime(2016, 1, 15); + + IReadOnlyList originalData = new List + { + new StoredUsage("category1", date.AddDays(1), Counters(10, 15)), + new StoredUsage("category1", date.AddDays(3), Counters(13, 18)), + new StoredUsage("category1", date.AddDays(5), Counters(15, 20)), + new StoredUsage("category1", date.AddDays(7), Counters(17, 22)) + }; + + A.CallTo(() => usageStore.QueryAsync($"{key}_API", new DateTime(2016, 1, 1), new DateTime(2016, 1, 15))) + .Returns(originalData); + + var result = await sut.GetMonthlyCallsAsync(key, date); + + Assert.Equal(55, result); + } + + [Fact] + public async Task Should_sum_up_when_getting_last_calls_calls() + { + var f = DateTime.Today; + var t = DateTime.Today.AddDays(10); + + IReadOnlyList originalData = new List + { + new StoredUsage("category1", f.AddDays(1), Counters(10, 15)), + new StoredUsage("category1", f.AddDays(3), Counters(13, 18)), + new StoredUsage("category1", f.AddDays(5), Counters(15, 20)), + new StoredUsage("category1", f.AddDays(7), Counters(17, 22)) + }; + + A.CallTo(() => usageStore.QueryAsync($"{key}_API", f, t)) + .Returns(originalData); + + var result = await sut.GetPreviousCallsAsync(key, f, t); + + Assert.Equal(55, result); + } + + [Fact] + public async Task Should_fill_missing_days() + { + var f = DateTime.Today; + var t = DateTime.Today.AddDays(4); + + var originalData = new List + { + new StoredUsage("MyCategory1", f.AddDays(1), Counters(10, 15)), + new StoredUsage("MyCategory1", f.AddDays(3), Counters(13, 18)), + new StoredUsage("MyCategory1", f.AddDays(4), Counters(15, 20)), + new StoredUsage(null, f.AddDays(0), Counters(17, 22)), + new StoredUsage(null, f.AddDays(2), Counters(11, 14)) + }; + + A.CallTo(() => usageStore.QueryAsync($"{key}_API", f, t)) + .Returns(originalData); + + var result = await sut.QueryAsync(key, f, t); + + var expected = new Dictionary> + { + ["MyCategory1"] = new List + { + new DateUsage(f.AddDays(0), 00, 00), + new DateUsage(f.AddDays(1), 10, 15), + new DateUsage(f.AddDays(2), 00, 00), + new DateUsage(f.AddDays(3), 13, 18), + new DateUsage(f.AddDays(4), 15, 20) + }, + ["*"] = new List + { + new DateUsage(f.AddDays(0), 17, 22), + new DateUsage(f.AddDays(1), 00, 00), + new DateUsage(f.AddDays(2), 11, 14), + new DateUsage(f.AddDays(3), 00, 00), + new DateUsage(f.AddDays(4), 00, 00) + } + }; + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_fill_missing_days_with_star() + { + var f = DateTime.Today; + var t = DateTime.Today.AddDays(4); + + A.CallTo(() => usageStore.QueryAsync($"{key}_API", f, t)) + .Returns(new List()); + + var result = await sut.QueryAsync(key, f, t); + + var expected = new Dictionary> + { + ["*"] = new List + { + new DateUsage(f.AddDays(0), 00, 00), + new DateUsage(f.AddDays(1), 00, 00), + new DateUsage(f.AddDays(2), 00, 00), + new DateUsage(f.AddDays(3), 00, 00), + new DateUsage(f.AddDays(4), 00, 00) + } + }; + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_not_track_if_weight_less_than_zero() + { + await sut.TrackAsync(key, "MyCategory", -1, 1000); + await sut.TrackAsync(key, "MyCategory", 0, 1000); + + sut.Next(); + sut.Dispose(); + + A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_aggregate_and_store_on_dispose() + { + var key1 = Guid.NewGuid().ToString(); + var key2 = Guid.NewGuid().ToString(); + var key3 = Guid.NewGuid().ToString(); + + var today = DateTime.Today; + + await sut.TrackAsync(key1, "MyCategory1", 1, 1000); + + await sut.TrackAsync(key2, "MyCategory1", 1.0, 2000); + await sut.TrackAsync(key2, "MyCategory1", 0.5, 3000); + + await sut.TrackAsync(key3, "MyCategory1", 0.3, 4000); + await sut.TrackAsync(key3, "MyCategory1", 0.1, 5000); + + await sut.TrackAsync(key3, null, 0.5, 2000); + await sut.TrackAsync(key3, null, 0.5, 6000); + + UsageUpdate[]? updates = null; + + A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) + .Invokes((UsageUpdate[] u) => updates = u); + + sut.Next(); + sut.Dispose(); + + updates.Should().BeEquivalentTo(new[] + { + new UsageUpdate(today, $"{key1}_API", "MyCategory1", Counters(1.0, 1000)), + new UsageUpdate(today, $"{key2}_API", "MyCategory1", Counters(1.5, 5000)), + new UsageUpdate(today, $"{key3}_API", "MyCategory1", Counters(0.4, 9000)), + new UsageUpdate(today, $"{key3}_API", "*", Counters(1, 8000)) + }, o => o.ComparingByMembers()); + + A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) + .MustHaveHappened(); + } + + private static Counters Counters(double count, long ms) + { + return new Counters + { + [BackgroundUsageTracker.CounterTotalCalls] = count, + [BackgroundUsageTracker.CounterTotalElapsedMs] = ms + }; + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs new file mode 100644 index 000000000..b6388cdd8 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs @@ -0,0 +1,81 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FluentAssertions; +using Squidex.Infrastructure.TestHelpers; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class ValidationExceptionTests + { + [Fact] + public void Should_format_message_from_summary() + { + var ex = new ValidationException("Summary."); + + Assert.Equal("Summary.", ex.Message); + } + + [Fact] + public void Should_append_dot_to_summary() + { + var ex = new ValidationException("Summary"); + + Assert.Equal("Summary.", ex.Message); + } + + [Fact] + public void Should_format_message_from_errors() + { + var ex = new ValidationException("Summary", new ValidationError("Error1."), new ValidationError("Error2.")); + + Assert.Equal("Summary: Error1. Error2.", ex.Message); + } + + [Fact] + public void Should_not_add_colon_twice() + { + var ex = new ValidationException("Summary:", new ValidationError("Error1."), new ValidationError("Error2.")); + + Assert.Equal("Summary: Error1. Error2.", ex.Message); + } + + [Fact] + public void Should_append_dots_to_errors() + { + var ex = new ValidationException("Summary", new ValidationError("Error1"), new ValidationError("Error2")); + + Assert.Equal("Summary: Error1. Error2.", ex.Message); + } + + [Fact] + public void Should_serialize_and_deserialize1() + { + var source = new ValidationException("Summary", new ValidationError("Error1"), null!); + var result = source.SerializeAndDeserializeBinary(); + + result.Errors.Should().BeEquivalentTo(source.Errors); + + Assert.Equal(source.Message, result.Message); + Assert.Equal(source.Summary, result.Summary); + } + + [Fact] + public void Should_serialize_and_deserialize() + { + var source = new ValidationException("Summary", new ValidationError("Error1"), new ValidationError("Error2")); + var result = source.SerializeAndDeserializeBinary(); + + result.Errors.Should().BeEquivalentTo(source.Errors); + + Assert.Equal(source.Message, result.Message); + Assert.Equal(source.Summary, result.Summary); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/ValidationExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/ValidationExtensionsTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/ValidationExtensionsTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/ValidationExtensionsTests.cs diff --git a/tests/Squidex.Web.Tests/ApiCostsAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiCostsAttributeTests.cs similarity index 100% rename from tests/Squidex.Web.Tests/ApiCostsAttributeTests.cs rename to backend/tests/Squidex.Web.Tests/ApiCostsAttributeTests.cs diff --git a/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs new file mode 100644 index 000000000..72c539c26 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs @@ -0,0 +1,123 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Security; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Web +{ + public class ApiExceptionFilterAttributeTests + { + private readonly ApiExceptionFilterAttribute sut = new ApiExceptionFilterAttribute(); + + [Fact] + public void Should_generate_404_for_DomainObjectNotFoundException() + { + var context = E(new DomainObjectNotFoundException("1", typeof(object))); + + sut.OnException(context); + + Assert.IsType(context.Result); + } + + [Fact] + public void Should_generate_400_for_ValidationException() + { + var ex = new ValidationException("NotAllowed", + new ValidationError("Error1"), + new ValidationError("Error2", "P"), + new ValidationError("Error3", "P1", "P2")); + + var context = E(ex); + + sut.OnException(context); + + var result = (ObjectResult)context.Result!; + + Assert.Equal(400, result.StatusCode); + Assert.Equal(400, (result.Value as ErrorDto)?.StatusCode); + + Assert.Equal(ex.Summary, (result.Value as ErrorDto)!.Message); + + Assert.Equal(new[] { "Error1", "P: Error2", "P1, P2: Error3" }, (result.Value as ErrorDto)!.Details); + } + + [Fact] + public void Should_generate_400_for_DomainException() + { + var context = E(new DomainException("NotAllowed")); + + sut.OnException(context); + + Validate(400, context); + } + + [Fact] + public void Should_generate_412_for_DomainObjectVersionException() + { + var context = E(new DomainObjectVersionException("1", typeof(object), 1, 2)); + + sut.OnException(context); + + Validate(412, context); + } + + [Fact] + public void Should_generate_403_for_DomainForbiddenException() + { + var context = E(new DomainForbiddenException("Forbidden")); + + sut.OnException(context); + + Validate(403, context); + } + + [Fact] + public void Should_generate_403_for_SecurityException() + { + var context = E(new SecurityException("Forbidden")); + + sut.OnException(context); + + Validate(403, context); + } + + private static ExceptionContext E(Exception exception) + { + var httpContext = new DefaultHttpContext(); + + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor + { + FilterDescriptors = new List() + }); + + return new ExceptionContext(actionContext, new List()) + { + Exception = exception + }; + } + + private static void Validate(int statusCode, ExceptionContext context) + { + var result = (ObjectResult)context.Result!; + + Assert.Equal(statusCode, result.StatusCode); + Assert.Equal(statusCode, (result.Value as ErrorDto)?.StatusCode); + + Assert.Equal(context.Exception.Message, (result.Value as ErrorDto)!.Message); + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs new file mode 100644 index 000000000..b647d275d --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Squidex.Shared; +using Squidex.Shared.Identity; +using Xunit; + +#pragma warning disable IDE0017 // Simplify object initialization + +namespace Squidex.Web +{ + public class ApiPermissionAttributeTests + { + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionExecutingContext actionExecutingContext; + private readonly ActionExecutionDelegate next; + private readonly ClaimsIdentity user = new ClaimsIdentity(); + private bool isNextCalled; + + public ApiPermissionAttributeTests() + { + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor + { + FilterDescriptors = new List() + }); + + actionExecutingContext = new ActionExecutingContext(actionContext, new List(), new Dictionary(), this); + actionExecutingContext.HttpContext = httpContext; + actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user); + + next = () => + { + isNextCalled = true; + + return Task.FromResult(null); + }; + } + + [Fact] + public void Should_use_bearer_schemes() + { + var sut = new ApiPermissionAttribute(); + + Assert.Equal("Bearer", sut.AuthenticationSchemes); + } + + [Fact] + public async Task Should_call_next_when_user_has_correct_permission() + { + actionExecutingContext.RouteData.Values["app"] = "my-app"; + + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); + + var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Null(actionExecutingContext.Result); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_return_forbidden_when_user_has_wrong_permission() + { + actionExecutingContext.RouteData.Values["app"] = "my-app"; + + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + + var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); + Assert.False(isNextCalled); + } + + [Fact] + public async Task Should_return_forbidden_when_route_data_has_no_value() + { + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + + var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); + Assert.False(isNextCalled); + } + + [Fact] + public async Task Should_return_forbidden_when_user_has_no_permission() + { + var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); + Assert.False(isNextCalled); + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs new file mode 100644 index 000000000..98729e997 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs @@ -0,0 +1,119 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Web.CommandMiddlewares +{ + public class ETagCommandMiddlewareTests + { + private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ETagCommandMiddleware sut; + + public ETagCommandMiddlewareTests() + { + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); + + sut = new ETagCommandMiddleware(httpContextAccessor); + } + + [Fact] + public async Task Should_do_nothing_when_context_is_null() + { + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(null!); + + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Null(command.Actor); + } + + [Fact] + public async Task Should_do_nothing_if_command_has_etag_defined() + { + httpContext.Request.Headers[HeaderNames.IfMatch] = "13"; + + var command = new CreateContent { ExpectedVersion = 1 }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(1, context.Command.ExpectedVersion); + } + + [Fact] + public async Task Should_add_expected_version_to_command() + { + httpContext.Request.Headers[HeaderNames.IfMatch] = "13"; + + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(13, context.Command.ExpectedVersion); + } + + [Fact] + public async Task Should_add_weak_etag_as_expected_version_to_command() + { + httpContext.Request.Headers[HeaderNames.IfMatch] = "W/13"; + + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(13, context.Command.ExpectedVersion); + } + + [Fact] + public async Task Should_add_version_from_result_as_etag_to_response() + { + var command = new CreateContent(); + var context = Ctx(command); + + context.Complete(new EntitySavedResult(17)); + + await sut.HandleAsync(context); + + Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext.Response.Headers[HeaderNames.ETag]); + } + + [Fact] + public async Task Should_add_version_from_entity_as_etag_to_response() + { + var command = new CreateContent(); + var context = Ctx(command); + + context.Complete(new ContentEntity { Version = 17 }); + + await sut.HandleAsync(context); + + Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext.Response.Headers[HeaderNames.ETag]); + } + + private CommandContext Ctx(ICommand command) + { + return new CommandContext(command, commandBus); + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs new file mode 100644 index 000000000..ec4bc4794 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs @@ -0,0 +1,114 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Security; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Security; +using Xunit; + +namespace Squidex.Web.CommandMiddlewares +{ + public class EnrichWithActorCommandMiddlewareTests + { + private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly EnrichWithActorCommandMiddleware sut; + + public EnrichWithActorCommandMiddlewareTests() + { + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); + + sut = new EnrichWithActorCommandMiddleware(httpContextAccessor); + } + + [Fact] + public async Task Should_throw_security_exception_when_no_subject_or_client_is_found() + { + var command = new CreateContent(); + var context = Ctx(command); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + } + + [Fact] + public async Task Should_do_nothing_when_context_is_null() + { + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(null!); + + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Null(command.Actor); + } + + [Fact] + public async Task Should_assign_actor_from_subject() + { + httpContext.User = CreatePrincipal(OpenIdClaims.Subject, "me"); + + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(new RefToken(RefTokenType.Subject, "me"), command.Actor); + } + + [Fact] + public async Task Should_assign_actor_from_client() + { + httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client"); + + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(new RefToken(RefTokenType.Client, "my-client"), command.Actor); + } + + [Fact] + public async Task Should_not_override_actor() + { + httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client"); + + var command = new CreateContent { Actor = new RefToken("subject", "me") }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(new RefToken("subject", "me"), command.Actor); + } + + private CommandContext Ctx(ICommand command) + { + return new CommandContext(command, commandBus); + } + + private static ClaimsPrincipal CreatePrincipal(string claimType, string claimValue) + { + var claimsPrincipal = new ClaimsPrincipal(); + var claimsIdentity = new ClaimsIdentity(); + + claimsIdentity.AddClaim(new Claim(claimType, claimValue)); + claimsPrincipal.AddIdentity(claimsIdentity); + + return claimsPrincipal; + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs new file mode 100644 index 000000000..b8f4017fd --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Web.CommandMiddlewares +{ + public class EnrichWithAppIdCommandMiddlewareTests + { + private readonly IContextProvider contextProvider = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly Context requestContext = Context.Anonymous(); + private readonly EnrichWithAppIdCommandMiddleware sut; + + public EnrichWithAppIdCommandMiddlewareTests() + { + A.CallTo(() => contextProvider.Context) + .Returns(requestContext); + + var app = A.Fake(); + + A.CallTo(() => app.Id).Returns(appId.Id); + A.CallTo(() => app.Name).Returns(appId.Name); + + requestContext.App = app; + + sut = new EnrichWithAppIdCommandMiddleware(contextProvider); + } + + [Fact] + public async Task Should_throw_exception_if_app_not_found() + { + requestContext.App = null!; + + var command = new CreateContent(); + var context = Ctx(command); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + } + + [Fact] + public async Task Should_assign_app_id_and_name_to_app_command() + { + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(appId, command.AppId); + } + + [Fact] + public async Task Should_assign_app_id_to_app_self_command() + { + var command = new ChangePlan(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(appId.Id, command.AppId); + } + + [Fact] + public async Task Should_not_override_app_id() + { + var command = new ChangePlan { AppId = Guid.NewGuid() }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.NotEqual(appId.Id, command.AppId); + } + + [Fact] + public async Task Should_not_override_app_id_and_name() + { + var command = new CreateContent { AppId = NamedId.Of(Guid.NewGuid(), "other-app") }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.NotEqual(appId, command.AppId); + } + + private CommandContext Ctx(ICommand command) + { + return new CommandContext(command, commandBus); + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs new file mode 100644 index 000000000..5328eb81e --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs @@ -0,0 +1,157 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Web.CommandMiddlewares +{ + public class EnrichWithSchemaIdCommandMiddlewareTests + { + private readonly IActionContextAccessor actionContextAccessor = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionContext actionContext = new ActionContext(); + private readonly EnrichWithSchemaIdCommandMiddleware sut; + + public EnrichWithSchemaIdCommandMiddlewareTests() + { + actionContext.RouteData = new RouteData(); + actionContext.HttpContext = httpContext; + + A.CallTo(() => actionContextAccessor.ActionContext) + .Returns(actionContext); + + var app = A.Fake(); + + A.CallTo(() => app.Id).Returns(appId.Id); + A.CallTo(() => app.Name).Returns(appId.Name); + + httpContext.Context().App = app; + + var schema = A.Fake(); + + A.CallTo(() => schema.Id).Returns(schemaId.Id); + A.CallTo(() => schema.SchemaDef).Returns(new Schema(schemaId.Name)); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name)) + .Returns(schema); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + .Returns(schema); + + sut = new EnrichWithSchemaIdCommandMiddleware(appProvider, actionContextAccessor); + } + + [Fact] + public async Task Should_throw_exception_if_schema_not_found() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, "other-schema")) + .Returns(Task.FromResult(null)); + + actionContext.RouteData.Values["name"] = "other-schema"; + + var command = new CreateContent { AppId = appId }; + var context = Ctx(command); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + } + + [Fact] + public async Task Should_do_nothing_when_route_has_no_parameter() + { + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Null(command.Actor); + } + + [Fact] + public async Task Should_assign_schema_id_and_name_from_name() + { + actionContext.RouteData.Values["name"] = schemaId.Name; + + var command = new CreateContent { AppId = appId }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(schemaId, command.SchemaId); + } + + [Fact] + public async Task Should_assign_schema_id_and_name_from_id() + { + actionContext.RouteData.Values["name"] = schemaId.Id; + + var command = new CreateContent { AppId = appId }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(schemaId, command.SchemaId); + } + + [Fact] + public async Task Should_assign_schema_id_from_id() + { + actionContext.RouteData.Values["name"] = schemaId.Name; + + var command = new UpdateSchema(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(schemaId.Id, command.SchemaId); + } + + [Fact] + public async Task Should_not_override_schema_id() + { + var command = new CreateSchema { SchemaId = Guid.NewGuid() }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.NotEqual(schemaId.Id, command.SchemaId); + } + + [Fact] + public async Task Should_not_override_schema_id_and_name() + { + var command = new CreateContent { SchemaId = NamedId.Of(Guid.NewGuid(), "other-schema") }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.NotEqual(appId, command.AppId); + } + + private CommandContext Ctx(ICommand command) + { + return new CommandContext(command, commandBus); + } + } +} diff --git a/tests/Squidex.Web.Tests/ExposedValuesTests.cs b/backend/tests/Squidex.Web.Tests/ExposedValuesTests.cs similarity index 100% rename from tests/Squidex.Web.Tests/ExposedValuesTests.cs rename to backend/tests/Squidex.Web.Tests/ExposedValuesTests.cs diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs new file mode 100644 index 000000000..75781d833 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs @@ -0,0 +1,167 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure.UsageTracking; +using Xunit; + +namespace Squidex.Web.Pipeline +{ + public class ApiCostsFilterTests + { + private readonly IActionContextAccessor actionContextAccessor = A.Fake(); + private readonly IAppEntity appEntity = A.Fake(); + private readonly IAppPlansProvider appPlansProvider = A.Fake(); + private readonly IUsageTracker usageTracker = A.Fake(); + private readonly IAppLimitsPlan appPlan = A.Fake(); + private readonly ActionExecutingContext actionContext; + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionExecutionDelegate next; + private readonly ApiCostsFilter sut; + private long apiCallsMax; + private long apiCallsCurrent; + private bool isNextCalled; + + public ApiCostsFilterTests() + { + actionContext = + new ActionExecutingContext( + new ActionContext(httpContext, new RouteData(), + new ActionDescriptor()), + new List(), new Dictionary(), null); + + A.CallTo(() => actionContextAccessor.ActionContext) + .Returns(actionContext); + + A.CallTo(() => appPlansProvider.GetPlan(null)) + .Returns(appPlan); + + A.CallTo(() => appPlansProvider.GetPlanForApp(appEntity)) + .Returns(appPlan); + + A.CallTo(() => appPlan.MaxApiCalls) + .ReturnsLazily(x => apiCallsMax); + + A.CallTo(() => usageTracker.GetMonthlyCallsAsync(A.Ignored, DateTime.Today)) + .ReturnsLazily(x => Task.FromResult(apiCallsCurrent)); + + next = () => + { + isNextCalled = true; + + return Task.FromResult(null); + }; + + sut = new ApiCostsFilter(appPlansProvider, usageTracker); + } + + [Fact] + public async Task Should_return_429_status_code_if_max_calls_over_limit() + { + sut.FilterDefinition = new ApiCostsAttribute(1); + + SetupApp(); + + apiCallsCurrent = 1000; + apiCallsMax = 600; + + await sut.OnActionExecutionAsync(actionContext, next); + + Assert.Equal(429, (actionContext.Result as StatusCodeResult)?.StatusCode); + Assert.False(isNextCalled); + + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_track_if_calls_left() + { + sut.FilterDefinition = new ApiCostsAttribute(13); + + SetupApp(); + + apiCallsCurrent = 1000; + apiCallsMax = 1600; + + await sut.OnActionExecutionAsync(actionContext, next); + + Assert.True(isNextCalled); + + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, 13, A.Ignored)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_allow_small_buffer() + { + sut.FilterDefinition = new ApiCostsAttribute(13); + + SetupApp(); + + apiCallsCurrent = 1099; + apiCallsMax = 1000; + + await sut.OnActionExecutionAsync(actionContext, next); + + Assert.True(isNextCalled); + + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, 13, A.Ignored)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_track_if_weight_is_zero() + { + sut.FilterDefinition = new ApiCostsAttribute(0); + + SetupApp(); + + apiCallsCurrent = 1000; + apiCallsMax = 600; + + await sut.OnActionExecutionAsync(actionContext, next); + + Assert.True(isNextCalled); + + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_track_if_app_not_defined() + { + sut.FilterDefinition = new ApiCostsAttribute(1); + + apiCallsCurrent = 1000; + apiCallsMax = 600; + + await sut.OnActionExecutionAsync(actionContext, next); + + Assert.True(isNextCalled); + + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + private void SetupApp() + { + httpContext.Context().App = appEntity; + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs similarity index 100% rename from tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs rename to backend/tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs new file mode 100644 index 000000000..274e21d50 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs @@ -0,0 +1,198 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure.Security; +using Squidex.Shared.Identity; +using Xunit; + +#pragma warning disable IDE0017 // Simplify object initialization + +namespace Squidex.Web.Pipeline +{ + public class AppResolverTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionContext actionContext; + private readonly ActionExecutingContext actionExecutingContext; + private readonly ActionExecutionDelegate next; + private readonly ClaimsIdentity user = new ClaimsIdentity(); + private readonly string appName = "my-app"; + private readonly AppResolver sut; + private bool isNextCalled; + + public AppResolverTests() + { + actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor + { + EndpointMetadata = new List() + }); + + actionExecutingContext = new ActionExecutingContext(actionContext, new List(), new Dictionary(), this); + actionExecutingContext.HttpContext = httpContext; + actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user); + actionExecutingContext.RouteData.Values["app"] = appName; + + next = () => + { + isNextCalled = true; + + return Task.FromResult(null); + }; + + sut = new AppResolver(appProvider); + } + + [Fact] + public async Task Should_return_not_found_if_app_not_found() + { + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(Task.FromResult(null)); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.IsType(actionExecutingContext.Result); + Assert.False(isNextCalled); + } + + [Fact] + public async Task Should_resolve_app_from_user() + { + var app = CreateApp(appName, appUser: "user1"); + + user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); + + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Same(app, httpContext.Context().App); + Assert.True(user.Claims.Count() > 2); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_resolve_app_from_client() + { + var app = CreateApp(appName, appClient: "client1"); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); + + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Same(app, httpContext.Context().App); + Assert.True(user.Claims.Count() > 2); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_resolve_app_if_anonymous_but_not_permissions() + { + var app = CreateApp(appName); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + + actionContext.ActionDescriptor.EndpointMetadata.Add(new AllowAnonymousAttribute()); + + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Same(app, httpContext.Context().App); + Assert.Equal(2, user.Claims.Count()); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_return_not_found_if_user_has_no_permissions() + { + var app = CreateApp(appName); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.IsType(actionExecutingContext.Result); + Assert.False(isNextCalled); + } + + [Fact] + public async Task Should_do_nothing_if_parameter_not_set() + { + actionExecutingContext.RouteData.Values.Remove("app"); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.True(isNextCalled); + + A.CallTo(() => appProvider.GetAppAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + private static IAppEntity CreateApp(string name, string? appUser = null, string? appClient = null) + { + var appEntity = A.Fake(); + + if (appUser != null) + { + A.CallTo(() => appEntity.Contributors) + .Returns(AppContributors.Empty.Assign(appUser, Role.Owner)); + } + else + { + A.CallTo(() => appEntity.Contributors) + .Returns(AppContributors.Empty); + } + + if (appClient != null) + { + A.CallTo(() => appEntity.Clients) + .Returns(AppClients.Empty.Add(appClient, "secret")); + } + else + { + A.CallTo(() => appEntity.Clients) + .Returns(AppClients.Empty); + } + + A.CallTo(() => appEntity.Name) + .Returns(name); + + A.CallTo(() => appEntity.Roles) + .Returns(Roles.Empty); + + return appEntity; + } + } +} diff --git a/tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs rename to backend/tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs new file mode 100644 index 000000000..b8080f243 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// 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 Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Squidex.Web.Pipeline +{ + public class ETagFilterTests + { + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionExecutingContext executingContext; + private readonly ActionExecutedContext executedContext; + private readonly ETagFilter sut = new ETagFilter(Options.Create(new ETagOptions())); + + public ETagFilterTests() + { + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + var filters = new List(); + + executingContext = new ActionExecutingContext(actionContext, filters, new Dictionary(), this); + executedContext = new ActionExecutedContext(actionContext, filters, this) + { + Result = new OkResult() + }; + } + + [Fact] + public async Task Should_not_convert_already_weak_tag() + { + httpContext.Response.Headers[HeaderNames.ETag] = "W/13"; + + await sut.OnActionExecutionAsync(executingContext, Next()); + + Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); + } + + [Fact] + public async Task Should_convert_strong_to_weak_tag() + { + httpContext.Response.Headers[HeaderNames.ETag] = "13"; + + await sut.OnActionExecutionAsync(executingContext, Next()); + + Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); + } + + [Fact] + public async Task Should_not_convert_empty_string_to_weak_tag() + { + httpContext.Response.Headers[HeaderNames.ETag] = string.Empty; + + await sut.OnActionExecutionAsync(executingContext, Next()); + + Assert.Equal(string.Empty, httpContext.Response.Headers[HeaderNames.ETag]); + } + + [Fact] + public async Task Should_return_304_for_same_etags() + { + httpContext.Request.Method = HttpMethods.Get; + httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/13"; + + httpContext.Response.Headers[HeaderNames.ETag] = "13"; + + await sut.OnActionExecutionAsync(executingContext, Next()); + + Assert.Equal(304, (executedContext.Result as StatusCodeResult)!.StatusCode); + } + + [Fact] + public async Task Should_not_return_304_for_different_etags() + { + httpContext.Request.Method = HttpMethods.Get; + httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/11"; + + httpContext.Response.Headers[HeaderNames.ETag] = "13"; + + await sut.OnActionExecutionAsync(executingContext, Next()); + + Assert.Equal(200, (executedContext.Result as StatusCodeResult)!.StatusCode); + } + + private ActionExecutionDelegate Next() + { + return () => Task.FromResult(executedContext); + } + } +} diff --git a/tests/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs rename to backend/tests/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs diff --git a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj new file mode 100644 index 000000000..eb724ae8c --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj @@ -0,0 +1,33 @@ + + + Exe + netcoreapp3.0 + Squidex.Web + 8.0 + enable + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + ..\..\Squidex.ruleset + + + + + diff --git a/tests/docker-compose.yml b/backend/tests/docker-compose.yml similarity index 100% rename from tests/docker-compose.yml rename to backend/tests/docker-compose.yml diff --git a/backend/tools/GenerateLanguages/GenerateLanguages.csproj b/backend/tools/GenerateLanguages/GenerateLanguages.csproj new file mode 100644 index 000000000..762a53b43 --- /dev/null +++ b/backend/tools/GenerateLanguages/GenerateLanguages.csproj @@ -0,0 +1,16 @@ + + + netcoreapp3.0 + Exe + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/tools/GenerateLanguages/GenerateLanguages.sln b/backend/tools/GenerateLanguages/GenerateLanguages.sln similarity index 100% rename from tools/GenerateLanguages/GenerateLanguages.sln rename to backend/tools/GenerateLanguages/GenerateLanguages.sln diff --git a/tools/GenerateLanguages/Program.cs b/backend/tools/GenerateLanguages/Program.cs similarity index 100% rename from tools/GenerateLanguages/Program.cs rename to backend/tools/GenerateLanguages/Program.cs diff --git a/backend/tools/LoadTest/LoadTest.csproj b/backend/tools/LoadTest/LoadTest.csproj new file mode 100644 index 000000000..6f04598b2 --- /dev/null +++ b/backend/tools/LoadTest/LoadTest.csproj @@ -0,0 +1,23 @@ + + + Exe + netcoreapp3.0 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + ..\..\Squidex.ruleset + + + + + diff --git a/tools/LoadTest/LoadTest.sln b/backend/tools/LoadTest/LoadTest.sln similarity index 100% rename from tools/LoadTest/LoadTest.sln rename to backend/tools/LoadTest/LoadTest.sln diff --git a/tools/LoadTest/Model/TestClient.cs b/backend/tools/LoadTest/Model/TestClient.cs similarity index 100% rename from tools/LoadTest/Model/TestClient.cs rename to backend/tools/LoadTest/Model/TestClient.cs diff --git a/tools/LoadTest/Model/TestEntity.cs b/backend/tools/LoadTest/Model/TestEntity.cs similarity index 100% rename from tools/LoadTest/Model/TestEntity.cs rename to backend/tools/LoadTest/Model/TestEntity.cs diff --git a/tools/LoadTest/ReadingBenchmarks.cs b/backend/tools/LoadTest/ReadingBenchmarks.cs similarity index 100% rename from tools/LoadTest/ReadingBenchmarks.cs rename to backend/tools/LoadTest/ReadingBenchmarks.cs diff --git a/tools/LoadTest/ReadingFixture.cs b/backend/tools/LoadTest/ReadingFixture.cs similarity index 100% rename from tools/LoadTest/ReadingFixture.cs rename to backend/tools/LoadTest/ReadingFixture.cs diff --git a/tools/LoadTest/Run.cs b/backend/tools/LoadTest/Run.cs similarity index 100% rename from tools/LoadTest/Run.cs rename to backend/tools/LoadTest/Run.cs diff --git a/tools/LoadTest/TestUtils.cs b/backend/tools/LoadTest/TestUtils.cs similarity index 100% rename from tools/LoadTest/TestUtils.cs rename to backend/tools/LoadTest/TestUtils.cs diff --git a/tools/LoadTest/Utils/Run.cs b/backend/tools/LoadTest/Utils/Run.cs similarity index 100% rename from tools/LoadTest/Utils/Run.cs rename to backend/tools/LoadTest/Utils/Run.cs diff --git a/tools/LoadTest/WritingBenchmarks.cs b/backend/tools/LoadTest/WritingBenchmarks.cs similarity index 100% rename from tools/LoadTest/WritingBenchmarks.cs rename to backend/tools/LoadTest/WritingBenchmarks.cs diff --git a/tools/LoadTest/WritingFixture.cs b/backend/tools/LoadTest/WritingFixture.cs similarity index 100% rename from tools/LoadTest/WritingFixture.cs rename to backend/tools/LoadTest/WritingFixture.cs diff --git a/backend/tools/Migrate_00/Migrate_00.csproj b/backend/tools/Migrate_00/Migrate_00.csproj new file mode 100644 index 000000000..bd4b2fa92 --- /dev/null +++ b/backend/tools/Migrate_00/Migrate_00.csproj @@ -0,0 +1,19 @@ + + + Exe + netcoreapp3.0 + 8.0 + enable + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/tools/Migrate_00/Program.cs b/backend/tools/Migrate_00/Program.cs similarity index 100% rename from tools/Migrate_00/Program.cs rename to backend/tools/Migrate_00/Program.cs diff --git a/backend/tools/Migrate_01/Migrate_01.csproj b/backend/tools/Migrate_01/Migrate_01.csproj new file mode 100644 index 000000000..c5d8b6620 --- /dev/null +++ b/backend/tools/Migrate_01/Migrate_01.csproj @@ -0,0 +1,25 @@ + + + netcoreapp3.0 + 8.0 + enable + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/backend/tools/Migrate_01/MigrationPath.cs b/backend/tools/Migrate_01/MigrationPath.cs new file mode 100644 index 000000000..6701d39ea --- /dev/null +++ b/backend/tools/Migrate_01/MigrationPath.cs @@ -0,0 +1,132 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Migrate_01.Migrations; +using Migrate_01.Migrations.MongoDb; +using Squidex.Infrastructure.Migrations; + +namespace Migrate_01 +{ + public sealed class MigrationPath : IMigrationPath + { + private const int CurrentVersion = 19; + private readonly IServiceProvider serviceProvider; + + public MigrationPath(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + public (int Version, IEnumerable? Migrations) GetNext(int version) + { + if (version == CurrentVersion) + { + return (CurrentVersion, null); + } + + var migrations = ResolveMigrators(version).Where(x => x != null).ToList(); + + return (CurrentVersion, migrations); + } + + private IEnumerable ResolveMigrators(int version) + { + yield return serviceProvider.GetRequiredService(); + + // Version 06: Convert Event store. Must always be executed first. + if (version < 6) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 07: Introduces AppId for backups. + else if (version < 7) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 05: Fixes the broken command architecture and requires a rebuild of all snapshots. + if (version < 5) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 12: Introduce roles. + else if (version < 12) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 09: Grain indexes. + if (version < 9) + { + yield return serviceProvider.GetService(); + } + + // Version 19: Unify indexes. + if (version < 19) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 11: Introduce content drafts. + if (version < 11) + { + yield return serviceProvider.GetService(); + yield return serviceProvider.GetRequiredService(); + } + + // Version 13: Json refactoring + if (version < 13) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 14: Schema refactoring + if (version < 14) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 01: Introduce app patterns. + if (version < 1) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 15: Introduce custom full text search actors. + if (version < 15) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 17: Rename slug field. + if (version < 17) + { + yield return serviceProvider.GetService(); + } + + // Version 18: Rebuild assets. + if (version < 18) + { + yield return serviceProvider.GetService(); + } + + // Version 16: Introduce file name slugs for assets. + if (version < 16) + { + yield return serviceProvider.GetRequiredService(); + } + + yield return serviceProvider.GetRequiredService(); + } + } +} diff --git a/backend/tools/Migrate_01/Migrations/AddPatterns.cs b/backend/tools/Migrate_01/Migrations/AddPatterns.cs new file mode 100644 index 000000000..f3f087a9c --- /dev/null +++ b/backend/tools/Migrate_01/Migrations/AddPatterns.cs @@ -0,0 +1,60 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Indexes; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Migrations; + +namespace Migrate_01.Migrations +{ + public sealed class AddPatterns : IMigration + { + private readonly InitialPatterns initialPatterns; + private readonly ICommandBus commandBus; + private readonly IAppsIndex indexForApps; + + public AddPatterns(InitialPatterns initialPatterns, ICommandBus commandBus, IAppsIndex indexForApps) + { + this.indexForApps = indexForApps; + this.initialPatterns = initialPatterns; + this.commandBus = commandBus; + } + + public async Task UpdateAsync() + { + var ids = await indexForApps.GetIdsAsync(); + + foreach (var id in ids) + { + var app = await indexForApps.GetAppAsync(id); + + if (app != null && app.Patterns.Count == 0) + { + foreach (var pattern in initialPatterns.Values) + { + var command = + new AddPattern + { + Actor = app.CreatedBy, + AppId = id, + Name = pattern.Name, + PatternId = Guid.NewGuid(), + Pattern = pattern.Pattern, + Message = pattern.Message + }; + + await commandBus.PublishAsync(command); + } + } + } + } + } +} \ No newline at end of file diff --git a/tools/Migrate_01/Migrations/ClearSchemas.cs b/backend/tools/Migrate_01/Migrations/ClearSchemas.cs similarity index 100% rename from tools/Migrate_01/Migrations/ClearSchemas.cs rename to backend/tools/Migrate_01/Migrations/ClearSchemas.cs diff --git a/backend/tools/Migrate_01/Migrations/ConvertEventStore.cs b/backend/tools/Migrate_01/Migrations/ConvertEventStore.cs new file mode 100644 index 000000000..990431f37 --- /dev/null +++ b/backend/tools/Migrate_01/Migrations/ConvertEventStore.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// 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 MongoDB.Bson; +using MongoDB.Driver; +using Newtonsoft.Json.Linq; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Migrations; + +namespace Migrate_01.Migrations +{ + public sealed class ConvertEventStore : IMigration + { + private readonly IEventStore eventStore; + + public ConvertEventStore(IEventStore eventStore) + { + this.eventStore = eventStore; + } + + public async Task UpdateAsync() + { + if (eventStore is MongoEventStore mongoEventStore) + { + var collection = mongoEventStore.RawCollection; + + var filter = Builders.Filter; + + var writesBatches = new List>(); + + async Task WriteAsync(WriteModel? model, bool force) + { + if (model != null) + { + writesBatches.Add(model); + } + + if (writesBatches.Count == 1000 || (force && writesBatches.Count > 0)) + { + await collection.BulkWriteAsync(writesBatches); + + writesBatches.Clear(); + } + } + + await collection.Find(new BsonDocument()).ForEachAsync(async commit => + { + foreach (BsonDocument @event in commit["Events"].AsBsonArray) + { + var meta = JObject.Parse(@event["Metadata"].AsString); + + @event.Remove("EventId"); + @event["Metadata"] = meta.ToBson(); + } + + await WriteAsync(new ReplaceOneModel(filter.Eq("_id", commit["_id"].AsString), commit), false); + }); + + await WriteAsync(null, true); + } + } + } +} diff --git a/backend/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs b/backend/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs new file mode 100644 index 000000000..3a0271125 --- /dev/null +++ b/backend/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs @@ -0,0 +1,97 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Migrations; + +namespace Migrate_01.Migrations +{ + public sealed class ConvertEventStoreAppId : IMigration + { + private readonly IEventStore eventStore; + + public ConvertEventStoreAppId(IEventStore eventStore) + { + this.eventStore = eventStore; + } + + public async Task UpdateAsync() + { + if (eventStore is MongoEventStore mongoEventStore) + { + var collection = mongoEventStore.RawCollection; + + var filterer = Builders.Filter; + var updater = Builders.Update; + + var writesBatches = new List>(); + + async Task WriteAsync(WriteModel? model, bool force) + { + if (model != null) + { + writesBatches.Add(model); + } + + if (writesBatches.Count == 1000 || (force && writesBatches.Count > 0)) + { + await collection.BulkWriteAsync(writesBatches); + + writesBatches.Clear(); + } + } + + await collection.Find(new BsonDocument()).ForEachAsync(async commit => + { + UpdateDefinition? update = null; + + var index = 0; + + foreach (BsonDocument @event in commit["Events"].AsBsonArray) + { + var data = JObject.Parse(@event["Payload"].AsString); + + if (data.TryGetValue("appId", out var appIdValue)) + { + var appId = NamedId.Parse(appIdValue.ToString(), Guid.TryParse).Id.ToString(); + + var eventUpdate = updater.Set($"Events.{index}.Metadata.{SquidexHeaders.AppId}", appId); + + if (update != null) + { + update = updater.Combine(update, eventUpdate); + } + else + { + update = eventUpdate; + } + } + + index++; + } + + if (update != null) + { + var write = new UpdateOneModel(filterer.Eq("_id", commit["_id"].AsString), update); + + await WriteAsync(write, false); + } + }); + + await WriteAsync(null, true); + } + } + } +} diff --git a/tools/Migrate_01/Migrations/CreateAssetSlugs.cs b/backend/tools/Migrate_01/Migrations/CreateAssetSlugs.cs similarity index 100% rename from tools/Migrate_01/Migrations/CreateAssetSlugs.cs rename to backend/tools/Migrate_01/Migrations/CreateAssetSlugs.cs diff --git a/tools/Migrate_01/Migrations/MongoDb/ConvertOldSnapshotStores.cs b/backend/tools/Migrate_01/Migrations/MongoDb/ConvertOldSnapshotStores.cs similarity index 100% rename from tools/Migrate_01/Migrations/MongoDb/ConvertOldSnapshotStores.cs rename to backend/tools/Migrate_01/Migrations/MongoDb/ConvertOldSnapshotStores.cs diff --git a/tools/Migrate_01/Migrations/MongoDb/ConvertRuleEventsJson.cs b/backend/tools/Migrate_01/Migrations/MongoDb/ConvertRuleEventsJson.cs similarity index 100% rename from tools/Migrate_01/Migrations/MongoDb/ConvertRuleEventsJson.cs rename to backend/tools/Migrate_01/Migrations/MongoDb/ConvertRuleEventsJson.cs diff --git a/tools/Migrate_01/Migrations/MongoDb/DeleteContentCollections.cs b/backend/tools/Migrate_01/Migrations/MongoDb/DeleteContentCollections.cs similarity index 100% rename from tools/Migrate_01/Migrations/MongoDb/DeleteContentCollections.cs rename to backend/tools/Migrate_01/Migrations/MongoDb/DeleteContentCollections.cs diff --git a/tools/Migrate_01/Migrations/MongoDb/RenameAssetSlugField.cs b/backend/tools/Migrate_01/Migrations/MongoDb/RenameAssetSlugField.cs similarity index 100% rename from tools/Migrate_01/Migrations/MongoDb/RenameAssetSlugField.cs rename to backend/tools/Migrate_01/Migrations/MongoDb/RenameAssetSlugField.cs diff --git a/tools/Migrate_01/Migrations/MongoDb/RestructureContentCollection.cs b/backend/tools/Migrate_01/Migrations/MongoDb/RestructureContentCollection.cs similarity index 100% rename from tools/Migrate_01/Migrations/MongoDb/RestructureContentCollection.cs rename to backend/tools/Migrate_01/Migrations/MongoDb/RestructureContentCollection.cs diff --git a/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs b/backend/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs similarity index 100% rename from tools/Migrate_01/Migrations/PopulateGrainIndexes.cs rename to backend/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs diff --git a/tools/Migrate_01/Migrations/RebuildApps.cs b/backend/tools/Migrate_01/Migrations/RebuildApps.cs similarity index 100% rename from tools/Migrate_01/Migrations/RebuildApps.cs rename to backend/tools/Migrate_01/Migrations/RebuildApps.cs diff --git a/tools/Migrate_01/Migrations/RebuildAssets.cs b/backend/tools/Migrate_01/Migrations/RebuildAssets.cs similarity index 100% rename from tools/Migrate_01/Migrations/RebuildAssets.cs rename to backend/tools/Migrate_01/Migrations/RebuildAssets.cs diff --git a/tools/Migrate_01/Migrations/RebuildContents.cs b/backend/tools/Migrate_01/Migrations/RebuildContents.cs similarity index 100% rename from tools/Migrate_01/Migrations/RebuildContents.cs rename to backend/tools/Migrate_01/Migrations/RebuildContents.cs diff --git a/tools/Migrate_01/Migrations/RebuildSnapshots.cs b/backend/tools/Migrate_01/Migrations/RebuildSnapshots.cs similarity index 100% rename from tools/Migrate_01/Migrations/RebuildSnapshots.cs rename to backend/tools/Migrate_01/Migrations/RebuildSnapshots.cs diff --git a/tools/Migrate_01/Migrations/StartEventConsumers.cs b/backend/tools/Migrate_01/Migrations/StartEventConsumers.cs similarity index 100% rename from tools/Migrate_01/Migrations/StartEventConsumers.cs rename to backend/tools/Migrate_01/Migrations/StartEventConsumers.cs diff --git a/tools/Migrate_01/Migrations/StopEventConsumers.cs b/backend/tools/Migrate_01/Migrations/StopEventConsumers.cs similarity index 100% rename from tools/Migrate_01/Migrations/StopEventConsumers.cs rename to backend/tools/Migrate_01/Migrations/StopEventConsumers.cs diff --git a/tools/Migrate_01/OldEvents/AppClientChanged.cs b/backend/tools/Migrate_01/OldEvents/AppClientChanged.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AppClientChanged.cs rename to backend/tools/Migrate_01/OldEvents/AppClientChanged.cs diff --git a/tools/Migrate_01/OldEvents/AppClientPermission.cs b/backend/tools/Migrate_01/OldEvents/AppClientPermission.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AppClientPermission.cs rename to backend/tools/Migrate_01/OldEvents/AppClientPermission.cs diff --git a/tools/Migrate_01/OldEvents/AppClientUpdated.cs b/backend/tools/Migrate_01/OldEvents/AppClientUpdated.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AppClientUpdated.cs rename to backend/tools/Migrate_01/OldEvents/AppClientUpdated.cs diff --git a/tools/Migrate_01/OldEvents/AppContributorAssigned.cs b/backend/tools/Migrate_01/OldEvents/AppContributorAssigned.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AppContributorAssigned.cs rename to backend/tools/Migrate_01/OldEvents/AppContributorAssigned.cs diff --git a/tools/Migrate_01/OldEvents/AppContributorPermission.cs b/backend/tools/Migrate_01/OldEvents/AppContributorPermission.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AppContributorPermission.cs rename to backend/tools/Migrate_01/OldEvents/AppContributorPermission.cs diff --git a/tools/Migrate_01/OldEvents/AppPlanChanged.cs b/backend/tools/Migrate_01/OldEvents/AppPlanChanged.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AppPlanChanged.cs rename to backend/tools/Migrate_01/OldEvents/AppPlanChanged.cs diff --git a/tools/Migrate_01/OldEvents/AppWorkflowConfigured.cs b/backend/tools/Migrate_01/OldEvents/AppWorkflowConfigured.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AppWorkflowConfigured.cs rename to backend/tools/Migrate_01/OldEvents/AppWorkflowConfigured.cs diff --git a/tools/Migrate_01/OldEvents/AssetRenamed.cs b/backend/tools/Migrate_01/OldEvents/AssetRenamed.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AssetRenamed.cs rename to backend/tools/Migrate_01/OldEvents/AssetRenamed.cs diff --git a/tools/Migrate_01/OldEvents/AssetTagged.cs b/backend/tools/Migrate_01/OldEvents/AssetTagged.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AssetTagged.cs rename to backend/tools/Migrate_01/OldEvents/AssetTagged.cs diff --git a/tools/Migrate_01/OldEvents/ContentArchived.cs b/backend/tools/Migrate_01/OldEvents/ContentArchived.cs similarity index 100% rename from tools/Migrate_01/OldEvents/ContentArchived.cs rename to backend/tools/Migrate_01/OldEvents/ContentArchived.cs diff --git a/tools/Migrate_01/OldEvents/ContentCreated.cs b/backend/tools/Migrate_01/OldEvents/ContentCreated.cs similarity index 100% rename from tools/Migrate_01/OldEvents/ContentCreated.cs rename to backend/tools/Migrate_01/OldEvents/ContentCreated.cs diff --git a/tools/Migrate_01/OldEvents/ContentPublished.cs b/backend/tools/Migrate_01/OldEvents/ContentPublished.cs similarity index 100% rename from tools/Migrate_01/OldEvents/ContentPublished.cs rename to backend/tools/Migrate_01/OldEvents/ContentPublished.cs diff --git a/tools/Migrate_01/OldEvents/ContentRestored.cs b/backend/tools/Migrate_01/OldEvents/ContentRestored.cs similarity index 100% rename from tools/Migrate_01/OldEvents/ContentRestored.cs rename to backend/tools/Migrate_01/OldEvents/ContentRestored.cs diff --git a/tools/Migrate_01/OldEvents/ContentStatusChanged.cs b/backend/tools/Migrate_01/OldEvents/ContentStatusChanged.cs similarity index 100% rename from tools/Migrate_01/OldEvents/ContentStatusChanged.cs rename to backend/tools/Migrate_01/OldEvents/ContentStatusChanged.cs diff --git a/tools/Migrate_01/OldEvents/ContentUnpublished.cs b/backend/tools/Migrate_01/OldEvents/ContentUnpublished.cs similarity index 100% rename from tools/Migrate_01/OldEvents/ContentUnpublished.cs rename to backend/tools/Migrate_01/OldEvents/ContentUnpublished.cs diff --git a/tools/Migrate_01/OldEvents/SchemaCreated.cs b/backend/tools/Migrate_01/OldEvents/SchemaCreated.cs similarity index 100% rename from tools/Migrate_01/OldEvents/SchemaCreated.cs rename to backend/tools/Migrate_01/OldEvents/SchemaCreated.cs diff --git a/tools/Migrate_01/OldEvents/ScriptsConfigured.cs b/backend/tools/Migrate_01/OldEvents/ScriptsConfigured.cs similarity index 100% rename from tools/Migrate_01/OldEvents/ScriptsConfigured.cs rename to backend/tools/Migrate_01/OldEvents/ScriptsConfigured.cs diff --git a/tools/Migrate_01/OldEvents/WebhookAdded.cs b/backend/tools/Migrate_01/OldEvents/WebhookAdded.cs similarity index 100% rename from tools/Migrate_01/OldEvents/WebhookAdded.cs rename to backend/tools/Migrate_01/OldEvents/WebhookAdded.cs diff --git a/tools/Migrate_01/OldEvents/WebhookDeleted.cs b/backend/tools/Migrate_01/OldEvents/WebhookDeleted.cs similarity index 100% rename from tools/Migrate_01/OldEvents/WebhookDeleted.cs rename to backend/tools/Migrate_01/OldEvents/WebhookDeleted.cs diff --git a/tools/Migrate_01/OldTriggers/AssetChangedTrigger.cs b/backend/tools/Migrate_01/OldTriggers/AssetChangedTrigger.cs similarity index 100% rename from tools/Migrate_01/OldTriggers/AssetChangedTrigger.cs rename to backend/tools/Migrate_01/OldTriggers/AssetChangedTrigger.cs diff --git a/tools/Migrate_01/OldTriggers/ContentChangedTrigger.cs b/backend/tools/Migrate_01/OldTriggers/ContentChangedTrigger.cs similarity index 100% rename from tools/Migrate_01/OldTriggers/ContentChangedTrigger.cs rename to backend/tools/Migrate_01/OldTriggers/ContentChangedTrigger.cs diff --git a/tools/Migrate_01/OldTriggers/ContentChangedTriggerSchema.cs b/backend/tools/Migrate_01/OldTriggers/ContentChangedTriggerSchema.cs similarity index 100% rename from tools/Migrate_01/OldTriggers/ContentChangedTriggerSchema.cs rename to backend/tools/Migrate_01/OldTriggers/ContentChangedTriggerSchema.cs diff --git a/tools/Migrate_01/RebuildOptions.cs b/backend/tools/Migrate_01/RebuildOptions.cs similarity index 100% rename from tools/Migrate_01/RebuildOptions.cs rename to backend/tools/Migrate_01/RebuildOptions.cs diff --git a/backend/tools/Migrate_01/RebuildRunner.cs b/backend/tools/Migrate_01/RebuildRunner.cs new file mode 100644 index 000000000..32dea1cbe --- /dev/null +++ b/backend/tools/Migrate_01/RebuildRunner.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Migrate_01.Migrations; +using Squidex.Infrastructure; + +namespace Migrate_01 +{ + public sealed class RebuildRunner + { + private readonly Rebuilder rebuilder; + private readonly PopulateGrainIndexes populateGrainIndexes; + private readonly RebuildOptions rebuildOptions; + + public RebuildRunner(Rebuilder rebuilder, IOptions rebuildOptions, PopulateGrainIndexes populateGrainIndexes) + { + Guard.NotNull(rebuilder); + Guard.NotNull(rebuildOptions); + Guard.NotNull(populateGrainIndexes); + + this.rebuilder = rebuilder; + this.rebuildOptions = rebuildOptions.Value; + this.populateGrainIndexes = populateGrainIndexes; + } + + public async Task RunAsync(CancellationToken ct) + { + if (rebuildOptions.Apps) + { + await rebuilder.RebuildAppsAsync(ct); + } + + if (rebuildOptions.Schemas) + { + await rebuilder.RebuildSchemasAsync(ct); + } + + if (rebuildOptions.Rules) + { + await rebuilder.RebuildRulesAsync(ct); + } + + if (rebuildOptions.Assets) + { + await rebuilder.RebuildAssetsAsync(ct); + } + + if (rebuildOptions.Contents) + { + await rebuilder.RebuildContentAsync(ct); + } + + if (rebuildOptions.Indexes) + { + await populateGrainIndexes.UpdateAsync(); + } + } + } +} diff --git a/tools/Migrate_01/Rebuilder.cs b/backend/tools/Migrate_01/Rebuilder.cs similarity index 100% rename from tools/Migrate_01/Rebuilder.cs rename to backend/tools/Migrate_01/Rebuilder.cs diff --git a/tools/Migrate_01/SquidexMigrations.cs b/backend/tools/Migrate_01/SquidexMigrations.cs similarity index 100% rename from tools/Migrate_01/SquidexMigrations.cs rename to backend/tools/Migrate_01/SquidexMigrations.cs diff --git a/build.ps1 b/build.ps1 index 5e33380b8..c6b27a268 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,11 +1,11 @@ # Build the image -docker build . -t squidex-build-image -f dockerfile.build +docker build . -t squidex-build-image -f dockerfile # Open the image docker create --name squidex-build-container squidex-build-image # Copy the output to the host file system -docker cp squidex-build-container:/out ./publish +docker cp squidex-build-container:/app/ ./publish # Cleanup docker rm squidex-build-container \ No newline at end of file diff --git a/build.sh b/build.sh index 1bd6b7e23..c6b27a268 100644 --- a/build.sh +++ b/build.sh @@ -1,11 +1,11 @@ # Build the image -docker build . -t squidex-build-image -f Dockerfile.build +docker build . -t squidex-build-image -f dockerfile # Open the image docker create --name squidex-build-container squidex-build-image # Copy the output to the host file system -docker cp squidex-build-container:/out ./publish +docker cp squidex-build-container:/app/ ./publish # Cleanup docker rm squidex-build-container \ No newline at end of file diff --git a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs b/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs deleted file mode 100644 index 1e5b02263..000000000 --- a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs +++ /dev/null @@ -1,134 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using Algolia.Search; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; - -#pragma warning disable IDE0059 // Value assigned to symbol is never used - -namespace Squidex.Extensions.Actions.Algolia -{ - public sealed class AlgoliaActionHandler : RuleActionHandler - { - private readonly ClientPool<(string AppId, string ApiKey, string IndexName), Index> clients; - - public AlgoliaActionHandler(RuleEventFormatter formatter) - : base(formatter) - { - clients = new ClientPool<(string AppId, string ApiKey, string IndexName), Index>(key => - { - var client = new AlgoliaClient(key.AppId, key.ApiKey); - - return client.InitIndex(key.IndexName); - }); - } - - protected override (string Description, AlgoliaJob Data) CreateJob(EnrichedEvent @event, AlgoliaAction action) - { - if (@event is EnrichedContentEvent contentEvent) - { - var contentId = contentEvent.Id.ToString(); - - var ruleDescription = string.Empty; - var ruleJob = new AlgoliaJob - { - AppId = action.AppId, - ApiKey = action.ApiKey, - ContentId = contentId, - IndexName = Format(action.IndexName, @event) - }; - - if (contentEvent.Type == EnrichedContentEventType.Deleted || - contentEvent.Type == EnrichedContentEventType.Unpublished) - { - ruleDescription = $"Delete entry from Algolia index: {action.IndexName}"; - } - else - { - ruleDescription = $"Add entry to Algolia index: {action.IndexName}"; - - JObject json; - try - { - string jsonString; - - if (!string.IsNullOrEmpty(action.Document)) - { - jsonString = Format(action.Document, @event)?.Trim(); - } - else - { - jsonString = ToJson(contentEvent); - } - - json = JObject.Parse(jsonString); - } - catch (Exception ex) - { - json = new JObject(new JProperty("error", $"Invalid JSON: {ex.Message}")); - } - - ruleJob.Content = json; - ruleJob.Content["objectID"] = contentId; - } - - return (ruleDescription, ruleJob); - } - - return ("Ignore", new AlgoliaJob()); - } - - protected override async Task ExecuteJobAsync(AlgoliaJob job, CancellationToken ct = default) - { - if (string.IsNullOrWhiteSpace(job.AppId)) - { - return Result.Ignored(); - } - - var index = clients.GetClient((job.AppId, job.ApiKey, job.IndexName)); - - try - { - if (job.Content != null) - { - var response = await index.PartialUpdateObjectAsync(job.Content, true, ct); - - return Result.Success(response.ToString(Formatting.Indented)); - } - else - { - var response = await index.DeleteObjectAsync(job.ContentId, ct); - - return Result.Success(response.ToString(Formatting.Indented)); - } - } - catch (AlgoliaException ex) - { - return Result.Failed(ex); - } - } - } - - public sealed class AlgoliaJob - { - public string AppId { get; set; } - - public string ApiKey { get; set; } - - public string ContentId { get; set; } - - public string IndexName { get; set; } - - public JObject Content { get; set; } - } -} diff --git a/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs b/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs deleted file mode 100644 index 9781e4ffe..000000000 --- a/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Infrastructure; - -namespace Squidex.Extensions.Actions.Fastly -{ - public sealed class FastlyActionHandler : RuleActionHandler - { - private const string Description = "Purge key in fastly"; - - private readonly IHttpClientFactory httpClientFactory; - - public FastlyActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) - : base(formatter) - { - Guard.NotNull(httpClientFactory, nameof(httpClientFactory)); - - this.httpClientFactory = httpClientFactory; - } - - protected override (string Description, FastlyJob Data) CreateJob(EnrichedEvent @event, FastlyAction action) - { - var id = @event is IEnrichedEntityEvent entityEvent ? entityEvent.Id.ToString() : string.Empty; - - var ruleJob = new FastlyJob - { - Key = id, - FastlyApiKey = action.ApiKey, - FastlyServiceID = action.ServiceId - }; - - return (Description, ruleJob); - } - - protected override async Task ExecuteJobAsync(FastlyJob job, CancellationToken ct = default) - { - using (var httpClient = httpClientFactory.CreateClient()) - { - httpClient.Timeout = TimeSpan.FromSeconds(2); - - var requestUrl = $"https://api.fastly.com/service/{job.FastlyServiceID}/purge/{job.Key}"; - var request = new HttpRequestMessage(HttpMethod.Post, requestUrl); - - request.Headers.Add("Fastly-Key", job.FastlyApiKey); - - return await httpClient.OneWayRequestAsync(request, ct: ct); - } - } - } - - public sealed class FastlyJob - { - public string FastlyApiKey { get; set; } - - public string FastlyServiceID { get; set; } - - public string Key { get; set; } - } -} diff --git a/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs b/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs deleted file mode 100644 index 0fbd3cdbf..000000000 --- a/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Infrastructure; - -namespace Squidex.Extensions.Actions.Slack -{ - public sealed class SlackActionHandler : RuleActionHandler - { - private const string Description = "Send message to slack"; - - private readonly IHttpClientFactory httpClientFactory; - - public SlackActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) - : base(formatter) - { - Guard.NotNull(httpClientFactory, nameof(httpClientFactory)); - - this.httpClientFactory = httpClientFactory; - } - - protected override (string Description, SlackJob Data) CreateJob(EnrichedEvent @event, SlackAction action) - { - var body = new { text = Format(action.Text, @event) }; - - var ruleJob = new SlackJob - { - RequestUrl = action.WebhookUrl.ToString(), - RequestBody = ToJson(body) - }; - - return (Description, ruleJob); - } - - protected override async Task ExecuteJobAsync(SlackJob job, CancellationToken ct = default) - { - using (var httpClient = httpClientFactory.CreateClient()) - { - httpClient.Timeout = TimeSpan.FromSeconds(2); - - var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) - { - Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") - }; - - return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); - } - } - } - - public sealed class SlackJob - { - public string RequestUrl { get; set; } - - public string RequestBody { get; set; } - } -} diff --git a/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs b/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs deleted file mode 100644 index 5ba6473b7..000000000 --- a/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using CoreTweet; -using Microsoft.Extensions.Options; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Infrastructure; - -namespace Squidex.Extensions.Actions.Twitter -{ - public sealed class TweetActionHandler : RuleActionHandler - { - private const string Description = "Send a tweet"; - - private readonly TwitterOptions twitterOptions; - - public TweetActionHandler(RuleEventFormatter formatter, IOptions twitterOptions) - : base(formatter) - { - Guard.NotNull(twitterOptions, nameof(twitterOptions)); - - this.twitterOptions = twitterOptions.Value; - } - - protected override (string Description, TweetJob Data) CreateJob(EnrichedEvent @event, TweetAction action) - { - var ruleJob = new TweetJob - { - Text = Format(action.Text, @event), - AccessToken = action.AccessToken, - AccessSecret = action.AccessSecret - }; - - return (Description, ruleJob); - } - - protected override async Task ExecuteJobAsync(TweetJob job, CancellationToken ct = default) - { - var tokens = Tokens.Create( - twitterOptions.ClientId, - twitterOptions.ClientSecret, - job.AccessToken, - job.AccessSecret); - - var request = new Dictionary - { - ["status"] = job.Text - }; - - await tokens.Statuses.UpdateAsync(request, ct); - - return Result.Success($"Tweeted: {job.Text}"); - } - } - - public sealed class TweetJob - { - public string AccessToken { get; set; } - - public string AccessSecret { get; set; } - - public string Text { get; set; } - } -} diff --git a/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs b/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs deleted file mode 100644 index d5e55991d..000000000 --- a/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Infrastructure; - -namespace Squidex.Extensions.Actions.Webhook -{ - public sealed class WebhookActionHandler : RuleActionHandler - { - private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(2); - private readonly IHttpClientFactory httpClientFactory; - - public WebhookActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) - : base(formatter) - { - Guard.NotNull(httpClientFactory, nameof(httpClientFactory)); - - this.httpClientFactory = httpClientFactory; - } - - protected override (string Description, WebhookJob Data) CreateJob(EnrichedEvent @event, WebhookAction action) - { - string requestBody; - - if (!string.IsNullOrEmpty(action.Payload)) - { - requestBody = Format(action.Payload, @event); - } - else - { - requestBody = ToEnvelopeJson(@event); - } - - var requestUrl = Format(action.Url, @event); - - var ruleDescription = $"Send event to webhook '{requestUrl}'"; - var ruleJob = new WebhookJob - { - RequestUrl = Format(action.Url.ToString(), @event), - RequestSignature = $"{requestBody}{action.SharedSecret}".Sha256Base64(), - RequestBody = requestBody - }; - - return (ruleDescription, ruleJob); - } - - protected override async Task ExecuteJobAsync(WebhookJob job, CancellationToken ct = default) - { - using (var httpClient = httpClientFactory.CreateClient()) - { - httpClient.Timeout = DefaultTimeout; - - var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) - { - Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") - }; - - request.Headers.Add("X-Signature", job.RequestSignature); - request.Headers.Add("X-Application", "Squidex Webhook"); - request.Headers.Add("User-Agent", "Squidex Webhook"); - - return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); - } - } - } - - public sealed class WebhookJob - { - public string RequestUrl { get; set; } - - public string RequestSignature { get; set; } - - public string RequestBody { get; set; } - } -} diff --git a/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/extensions/Squidex.Extensions/Squidex.Extensions.csproj deleted file mode 100644 index 257aeaa39..000000000 --- a/extensions/Squidex.Extensions/Squidex.Extensions.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - netstandard2.0 - 7.3 - - - - - - - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex/.sass-lint.yml b/frontend/.sass-lint.yml similarity index 100% rename from src/Squidex/.sass-lint.yml rename to frontend/.sass-lint.yml diff --git a/src/Squidex/app-config/karma-test-shim.js b/frontend/app-config/karma-test-shim.js similarity index 100% rename from src/Squidex/app-config/karma-test-shim.js rename to frontend/app-config/karma-test-shim.js diff --git a/src/Squidex/app-config/karma.conf.js b/frontend/app-config/karma.conf.js similarity index 100% rename from src/Squidex/app-config/karma.conf.js rename to frontend/app-config/karma.conf.js diff --git a/src/Squidex/app-config/karma.coverage.conf.js b/frontend/app-config/karma.coverage.conf.js similarity index 100% rename from src/Squidex/app-config/karma.coverage.conf.js rename to frontend/app-config/karma.coverage.conf.js diff --git a/frontend/app-config/webpack.config.js b/frontend/app-config/webpack.config.js new file mode 100644 index 000000000..f569ea05a --- /dev/null +++ b/frontend/app-config/webpack.config.js @@ -0,0 +1,376 @@ +const webpack = require('webpack'), + path = require('path'); + +const appRoot = path.resolve(__dirname, '..'); + +function root() { + var newArgs = Array.prototype.slice.call(arguments, 0); + + return path.join.apply(path, [appRoot].concat(newArgs)); +}; + +const plugins = { + // https://github.com/webpack-contrib/mini-css-extract-plugin + MiniCssExtractPlugin: require('mini-css-extract-plugin'), + // https://github.com/dividab/tsconfig-paths-webpack-plugin + TsconfigPathsPlugin: require('tsconfig-paths-webpack-plugin'), + // https://github.com/aackerman/circular-dependency-plugin + CircularDependencyPlugin: require('circular-dependency-plugin'), + // https://github.com/jantimon/html-webpack-plugin + HtmlWebpackPlugin: require('html-webpack-plugin'), + // https://webpack.js.org/plugins/terser-webpack-plugin/ + TerserPlugin: require('terser-webpack-plugin'), + // https://www.npmjs.com/package/@ngtools/webpack + NgToolsWebpack: require('@ngtools/webpack'), + // https://github.com/NMFR/optimize-css-assets-webpack-plugin + OptimizeCSSAssetsPlugin: require("optimize-css-assets-webpack-plugin"), + // https://github.com/jrparish/tslint-webpack-plugin + TsLintPlugin: require('tslint-webpack-plugin') +}; + +module.exports = function (env) { + const isDevServer = path.basename(require.main.filename) === 'webpack-dev-server.js'; + const isProduction = env && env.production; + const isTests = env && env.target === 'tests'; + const isCoverage = env && env.coverage; + const isAot = isProduction; + + const config = { + mode: isProduction ? 'production' : 'development', + + /** + * Source map for Karma from the help of karma-sourcemap-loader & karma-webpack. + * + * See: https://webpack.js.org/configuration/devtool/ + */ + devtool: isProduction ? false : 'inline-source-map', + + /** + * Options affecting the resolving of modules. + * + * See: https://webpack.js.org/configuration/resolve/ + */ + resolve: { + /** + * An array of extensions that should be used to resolve modules. + * + * See: https://webpack.js.org/configuration/resolve/#resolve-extensions + */ + extensions: ['.ts', '.js', '.mjs', '.css', '.scss'], + modules: [ + root('app'), + root('app', 'theme'), + root('node_modules') + ], + + plugins: [ + new plugins.TsconfigPathsPlugin() + ] + }, + + /** + * Options affecting the normal modules. + * + * See: https://webpack.js.org/configuration/module/ + */ + module: { + /** + * An array of Rules which are matched to requests when modules are created. + * + * See: https://webpack.js.org/configuration/module/#module-rules + */ + rules: [{ + test: /\.mjs$/, + type: "javascript/auto", + include: [/node_modules/] + }, { + test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/, + parser: { system: true }, + include: [/node_modules/] + }, { + test: /\.js\.flow$/, + use: [{ + loader: 'ignore-loader' + }], + include: [/node_modules/] + }, { + test: /\.map$/, + use: [{ + loader: 'ignore-loader' + }], + include: [/node_modules/] + }, { + test: /\.d\.ts$/, + use: [{ + loader: 'ignore-loader' + }], + include: [/node_modules/] + }, { + test: /\.(woff|woff2|ttf|eot)(\?.*$|$)/, + use: [{ + loader: 'file-loader?name=[name].[hash].[ext]', + options: { + outputPath: 'assets', + /* + * Use custom public path as ./ is not supported by fonts. + */ + publicPath: isDevServer ? undefined : 'assets' + } + }] + }, { + test: /\.(png|jpe?g|gif|svg|ico)(\?.*$|$)/, + use: [{ + loader: 'file-loader?name=[name].[hash].[ext]', + options: { + outputPath: 'assets' + } + }] + }, { + test: /\.css$/, + use: [ + plugins.MiniCssExtractPlugin.loader, + { + loader: 'css-loader' + }] + }, { + test: /\.scss$/, + use: [{ + loader: 'raw-loader' + }, { + loader: 'sass-loader', options: { + sassOptions: { + includePaths: [root('app', 'theme')] + } + } + }], + exclude: root('app', 'theme') + }] + }, + + plugins: [ + new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)fesm5/, root('./app'), {}), + new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/), + + /** + * Puts each bundle into a file and appends the hash of the file to the path. + * + * See: https://github.com/webpack-contrib/mini-css-extract-plugin + */ + new plugins.MiniCssExtractPlugin('[name].css'), + + new webpack.LoaderOptionsPlugin({ + options: { + htmlLoader: { + /** + * Define the root for images, so that we can use absolute urls. + * + * See: https://github.com/webpack/html-loader#Advanced_Options + */ + root: root('app', 'images') + }, + context: '/' + } + }), + + /** + * Detect circular dependencies in app. + * + * See: https://github.com/aackerman/circular-dependency-plugin + */ + new plugins.CircularDependencyPlugin({ + exclude: /([\\\/]node_modules[\\\/])|(ngfactory\.js$)/, + // Add errors to webpack instead of warnings + failOnError: true + }), + ], + + devServer: { + headers: { + 'Access-Control-Allow-Origin': '*' + }, + historyApiFallback: true + } + }; + + if (!isTests) { + /** + * The entry point for the bundle. Our Angular app. + * + * See: https://webpack.js.org/configuration/entry-context/ + */ + config.entry = { + 'shims': './app/shims.ts', + 'app': './app/app.ts' + }; + + if (isProduction) { + config.output = { + /** + * The output directory as absolute path (required). + * + * See: https://webpack.js.org/configuration/output/#output-path + */ + path: root('/build/'), + + publicPath: './build/', + + /** + * Specifies the name of each output file on disk. + * + * See: https://webpack.js.org/configuration/output/#output-filename + */ + filename: '[name].js', + + /** + * The filename of non-entry chunks as relative path inside the output.path directory. + * + * See: https://webpack.js.org/configuration/output/#output-chunkfilename + */ + chunkFilename: '[id].[hash].chunk.js' + }; + } else { + config.output = { + filename: '[name].js', + + /** + * Set the public path, because we are running the website from another port (5000). + */ + publicPath: 'http://localhost:3000/' + }; + } + + config.plugins.push( + new plugins.HtmlWebpackPlugin({ + hash: true, + chunks: ['shims', 'app'], + chunksSortMode: 'manual', + template: 'app/index.html' + }) + ); + + config.plugins.push( + new plugins.HtmlWebpackPlugin({ + template: 'app/_theme.html', hash: true, chunksSortMode: 'none', filename: 'theme.html' + }) + ); + + config.plugins.push( + new plugins.TsLintPlugin({ + files: ['./app/**/*.ts'], + /** + * Path to a configuration file. + */ + config: root('tslint.json'), + /** + * Wait for linting and fail the build when linting error occur. + */ + waitForLinting: isProduction + }) + ); + } + + if (!isCoverage) { + config.plugins.push( + new plugins.NgToolsWebpack.AngularCompilerPlugin({ + directTemplateLoading: true, + entryModule: 'app/app.module#AppModule', + sourceMap: !isProduction, + skipCodeGeneration: !isAot, + tsConfigPath: './tsconfig.json' + }) + ); + } + + if (isProduction) { + config.optimization = { + minimizer: [ + new plugins.TerserPlugin({ + terserOptions: { + compress: true, + ecma: 5, + mangle: true, + output: { + comments: false + }, + safari10: true + }, + extractComments: true + }), + + new plugins.OptimizeCSSAssetsPlugin({}) + ] + }; + + config.performance = { + hints: false + }; + } + + if (isCoverage) { + // Do not instrument tests. + config.module.rules.push({ + test: /\.ts$/, + use: [{ + loader: 'ts-loader' + }], + include: [/\.(e2e|spec)\.ts$/], + }); + + // Use instrument loader for all normal files. + config.module.rules.push({ + test: /\.ts$/, + use: [{ + loader: 'istanbul-instrumenter-loader?esModules=true' + }, { + loader: 'ts-loader' + }], + exclude: [/\.(e2e|spec)\.ts$/] + }); + } else { + config.module.rules.push({ + test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, + use: [{ + loader: plugins.NgToolsWebpack.NgToolsLoader + }] + }) + } + + if (isProduction) { + config.module.rules.push({ + test: /\.scss$/, + /* + * Extract the content from a bundle to a file. + * + * See: https://github.com/webpack-contrib/extract-text-webpack-plugin + */ + use: [ + plugins.MiniCssExtractPlugin.loader, + { + loader: 'css-loader' + }, { + loader: 'sass-loader' + }], + /* + * Do not include component styles. + */ + include: root('app', 'theme'), + }); + } else { + config.module.rules.push({ + test: /\.scss$/, + use: [{ + loader: 'style-loader' + }, { + loader: 'css-loader' + }, { + loader: 'sass-loader?sourceMap' + }], + /* + * Do not include component styles. + */ + include: root('app', 'theme') + }); + } + + return config; +}; \ No newline at end of file diff --git a/src/Squidex/wwwroot/_theme.html b/frontend/app/_theme.html similarity index 100% rename from src/Squidex/wwwroot/_theme.html rename to frontend/app/_theme.html diff --git a/src/Squidex/app/app.component.html b/frontend/app/app.component.html similarity index 100% rename from src/Squidex/app/app.component.html rename to frontend/app/app.component.html diff --git a/src/Squidex/app/app.component.scss b/frontend/app/app.component.scss similarity index 100% rename from src/Squidex/app/app.component.scss rename to frontend/app/app.component.scss diff --git a/src/Squidex/app/app.component.ts b/frontend/app/app.component.ts similarity index 100% rename from src/Squidex/app/app.component.ts rename to frontend/app/app.component.ts diff --git a/src/Squidex/app/app.module.ts b/frontend/app/app.module.ts similarity index 100% rename from src/Squidex/app/app.module.ts rename to frontend/app/app.module.ts diff --git a/src/Squidex/app/app.routes.ts b/frontend/app/app.routes.ts similarity index 100% rename from src/Squidex/app/app.routes.ts rename to frontend/app/app.routes.ts diff --git a/src/Squidex/app/app.ts b/frontend/app/app.ts similarity index 100% rename from src/Squidex/app/app.ts rename to frontend/app/app.ts diff --git a/src/Squidex/app/declarations.d.ts b/frontend/app/declarations.d.ts similarity index 100% rename from src/Squidex/app/declarations.d.ts rename to frontend/app/declarations.d.ts diff --git a/src/Squidex/app/features/administration/administration-area.component.html b/frontend/app/features/administration/administration-area.component.html similarity index 100% rename from src/Squidex/app/features/administration/administration-area.component.html rename to frontend/app/features/administration/administration-area.component.html diff --git a/src/Squidex/app/features/administration/administration-area.component.scss b/frontend/app/features/administration/administration-area.component.scss similarity index 100% rename from src/Squidex/app/features/administration/administration-area.component.scss rename to frontend/app/features/administration/administration-area.component.scss diff --git a/src/Squidex/app/features/administration/administration-area.component.ts b/frontend/app/features/administration/administration-area.component.ts similarity index 100% rename from src/Squidex/app/features/administration/administration-area.component.ts rename to frontend/app/features/administration/administration-area.component.ts diff --git a/src/Squidex/app/features/administration/declarations.ts b/frontend/app/features/administration/declarations.ts similarity index 100% rename from src/Squidex/app/features/administration/declarations.ts rename to frontend/app/features/administration/declarations.ts diff --git a/src/Squidex/app/features/administration/guards/unset-user.guard.spec.ts b/frontend/app/features/administration/guards/unset-user.guard.spec.ts similarity index 100% rename from src/Squidex/app/features/administration/guards/unset-user.guard.spec.ts rename to frontend/app/features/administration/guards/unset-user.guard.spec.ts diff --git a/src/Squidex/app/features/administration/guards/unset-user.guard.ts b/frontend/app/features/administration/guards/unset-user.guard.ts similarity index 100% rename from src/Squidex/app/features/administration/guards/unset-user.guard.ts rename to frontend/app/features/administration/guards/unset-user.guard.ts diff --git a/src/Squidex/app/features/administration/guards/user-must-exist.guard.spec.ts b/frontend/app/features/administration/guards/user-must-exist.guard.spec.ts similarity index 100% rename from src/Squidex/app/features/administration/guards/user-must-exist.guard.spec.ts rename to frontend/app/features/administration/guards/user-must-exist.guard.spec.ts diff --git a/src/Squidex/app/features/administration/guards/user-must-exist.guard.ts b/frontend/app/features/administration/guards/user-must-exist.guard.ts similarity index 100% rename from src/Squidex/app/features/administration/guards/user-must-exist.guard.ts rename to frontend/app/features/administration/guards/user-must-exist.guard.ts diff --git a/src/Squidex/app/features/administration/internal.ts b/frontend/app/features/administration/internal.ts similarity index 100% rename from src/Squidex/app/features/administration/internal.ts rename to frontend/app/features/administration/internal.ts diff --git a/src/Squidex/app/features/administration/module.ts b/frontend/app/features/administration/module.ts similarity index 100% rename from src/Squidex/app/features/administration/module.ts rename to frontend/app/features/administration/module.ts diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumer.component.ts b/frontend/app/features/administration/pages/event-consumers/event-consumer.component.ts similarity index 100% rename from src/Squidex/app/features/administration/pages/event-consumers/event-consumer.component.ts rename to frontend/app/features/administration/pages/event-consumers/event-consumer.component.ts diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html b/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.html similarity index 100% rename from src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html rename to frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.html diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.scss b/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.scss similarity index 100% rename from src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.scss rename to frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.scss diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts b/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.ts similarity index 100% rename from src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts rename to frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.ts diff --git a/src/Squidex/app/features/administration/pages/restore/restore-page.component.html b/frontend/app/features/administration/pages/restore/restore-page.component.html similarity index 100% rename from src/Squidex/app/features/administration/pages/restore/restore-page.component.html rename to frontend/app/features/administration/pages/restore/restore-page.component.html diff --git a/src/Squidex/app/features/administration/pages/restore/restore-page.component.scss b/frontend/app/features/administration/pages/restore/restore-page.component.scss similarity index 100% rename from src/Squidex/app/features/administration/pages/restore/restore-page.component.scss rename to frontend/app/features/administration/pages/restore/restore-page.component.scss diff --git a/src/Squidex/app/features/administration/pages/restore/restore-page.component.ts b/frontend/app/features/administration/pages/restore/restore-page.component.ts similarity index 100% rename from src/Squidex/app/features/administration/pages/restore/restore-page.component.ts rename to frontend/app/features/administration/pages/restore/restore-page.component.ts diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.html b/frontend/app/features/administration/pages/users/user-page.component.html similarity index 100% rename from src/Squidex/app/features/administration/pages/users/user-page.component.html rename to frontend/app/features/administration/pages/users/user-page.component.html diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.scss b/frontend/app/features/administration/pages/users/user-page.component.scss similarity index 100% rename from src/Squidex/app/features/administration/pages/users/user-page.component.scss rename to frontend/app/features/administration/pages/users/user-page.component.scss diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.ts b/frontend/app/features/administration/pages/users/user-page.component.ts similarity index 100% rename from src/Squidex/app/features/administration/pages/users/user-page.component.ts rename to frontend/app/features/administration/pages/users/user-page.component.ts diff --git a/src/Squidex/app/features/administration/pages/users/user.component.ts b/frontend/app/features/administration/pages/users/user.component.ts similarity index 100% rename from src/Squidex/app/features/administration/pages/users/user.component.ts rename to frontend/app/features/administration/pages/users/user.component.ts diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.html b/frontend/app/features/administration/pages/users/users-page.component.html similarity index 100% rename from src/Squidex/app/features/administration/pages/users/users-page.component.html rename to frontend/app/features/administration/pages/users/users-page.component.html diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.scss b/frontend/app/features/administration/pages/users/users-page.component.scss similarity index 100% rename from src/Squidex/app/features/administration/pages/users/users-page.component.scss rename to frontend/app/features/administration/pages/users/users-page.component.scss diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.ts b/frontend/app/features/administration/pages/users/users-page.component.ts similarity index 100% rename from src/Squidex/app/features/administration/pages/users/users-page.component.ts rename to frontend/app/features/administration/pages/users/users-page.component.ts diff --git a/src/Squidex/app/features/administration/services/event-consumers.service.spec.ts b/frontend/app/features/administration/services/event-consumers.service.spec.ts similarity index 100% rename from src/Squidex/app/features/administration/services/event-consumers.service.spec.ts rename to frontend/app/features/administration/services/event-consumers.service.spec.ts diff --git a/src/Squidex/app/features/administration/services/event-consumers.service.ts b/frontend/app/features/administration/services/event-consumers.service.ts similarity index 100% rename from src/Squidex/app/features/administration/services/event-consumers.service.ts rename to frontend/app/features/administration/services/event-consumers.service.ts diff --git a/src/Squidex/app/features/administration/services/users.service.spec.ts b/frontend/app/features/administration/services/users.service.spec.ts similarity index 100% rename from src/Squidex/app/features/administration/services/users.service.spec.ts rename to frontend/app/features/administration/services/users.service.spec.ts diff --git a/src/Squidex/app/features/administration/services/users.service.ts b/frontend/app/features/administration/services/users.service.ts similarity index 100% rename from src/Squidex/app/features/administration/services/users.service.ts rename to frontend/app/features/administration/services/users.service.ts diff --git a/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts b/frontend/app/features/administration/state/event-consumers.state.spec.ts similarity index 100% rename from src/Squidex/app/features/administration/state/event-consumers.state.spec.ts rename to frontend/app/features/administration/state/event-consumers.state.spec.ts diff --git a/src/Squidex/app/features/administration/state/event-consumers.state.ts b/frontend/app/features/administration/state/event-consumers.state.ts similarity index 100% rename from src/Squidex/app/features/administration/state/event-consumers.state.ts rename to frontend/app/features/administration/state/event-consumers.state.ts diff --git a/src/Squidex/app/features/administration/state/users.forms.ts b/frontend/app/features/administration/state/users.forms.ts similarity index 100% rename from src/Squidex/app/features/administration/state/users.forms.ts rename to frontend/app/features/administration/state/users.forms.ts diff --git a/src/Squidex/app/features/administration/state/users.state.spec.ts b/frontend/app/features/administration/state/users.state.spec.ts similarity index 100% rename from src/Squidex/app/features/administration/state/users.state.spec.ts rename to frontend/app/features/administration/state/users.state.spec.ts diff --git a/src/Squidex/app/features/administration/state/users.state.ts b/frontend/app/features/administration/state/users.state.ts similarity index 100% rename from src/Squidex/app/features/administration/state/users.state.ts rename to frontend/app/features/administration/state/users.state.ts diff --git a/src/Squidex/app/features/api/api-area.component.html b/frontend/app/features/api/api-area.component.html similarity index 100% rename from src/Squidex/app/features/api/api-area.component.html rename to frontend/app/features/api/api-area.component.html diff --git a/src/Squidex/app/features/api/api-area.component.scss b/frontend/app/features/api/api-area.component.scss similarity index 100% rename from src/Squidex/app/features/api/api-area.component.scss rename to frontend/app/features/api/api-area.component.scss diff --git a/src/Squidex/app/features/api/api-area.component.ts b/frontend/app/features/api/api-area.component.ts similarity index 100% rename from src/Squidex/app/features/api/api-area.component.ts rename to frontend/app/features/api/api-area.component.ts diff --git a/src/Squidex/app/features/api/declarations.ts b/frontend/app/features/api/declarations.ts similarity index 100% rename from src/Squidex/app/features/api/declarations.ts rename to frontend/app/features/api/declarations.ts diff --git a/src/Squidex/app/features/api/index.ts b/frontend/app/features/api/index.ts similarity index 100% rename from src/Squidex/app/features/api/index.ts rename to frontend/app/features/api/index.ts diff --git a/src/Squidex/app/features/api/module.ts b/frontend/app/features/api/module.ts similarity index 100% rename from src/Squidex/app/features/api/module.ts rename to frontend/app/features/api/module.ts diff --git a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.html b/frontend/app/features/api/pages/graphql/graphql-page.component.html similarity index 100% rename from src/Squidex/app/features/api/pages/graphql/graphql-page.component.html rename to frontend/app/features/api/pages/graphql/graphql-page.component.html diff --git a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.scss b/frontend/app/features/api/pages/graphql/graphql-page.component.scss similarity index 100% rename from src/Squidex/app/features/api/pages/graphql/graphql-page.component.scss rename to frontend/app/features/api/pages/graphql/graphql-page.component.scss diff --git a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts b/frontend/app/features/api/pages/graphql/graphql-page.component.ts similarity index 100% rename from src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts rename to frontend/app/features/api/pages/graphql/graphql-page.component.ts diff --git a/src/Squidex/app/features/apps/declarations.ts b/frontend/app/features/apps/declarations.ts similarity index 100% rename from src/Squidex/app/features/apps/declarations.ts rename to frontend/app/features/apps/declarations.ts diff --git a/src/Squidex/app/features/apps/index.ts b/frontend/app/features/apps/index.ts similarity index 100% rename from src/Squidex/app/features/apps/index.ts rename to frontend/app/features/apps/index.ts diff --git a/src/Squidex/app/features/apps/module.ts b/frontend/app/features/apps/module.ts similarity index 100% rename from src/Squidex/app/features/apps/module.ts rename to frontend/app/features/apps/module.ts diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.html b/frontend/app/features/apps/pages/apps-page.component.html similarity index 100% rename from src/Squidex/app/features/apps/pages/apps-page.component.html rename to frontend/app/features/apps/pages/apps-page.component.html diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.scss b/frontend/app/features/apps/pages/apps-page.component.scss similarity index 100% rename from src/Squidex/app/features/apps/pages/apps-page.component.scss rename to frontend/app/features/apps/pages/apps-page.component.scss diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.ts b/frontend/app/features/apps/pages/apps-page.component.ts similarity index 100% rename from src/Squidex/app/features/apps/pages/apps-page.component.ts rename to frontend/app/features/apps/pages/apps-page.component.ts diff --git a/src/Squidex/app/features/apps/pages/news-dialog.component.html b/frontend/app/features/apps/pages/news-dialog.component.html similarity index 100% rename from src/Squidex/app/features/apps/pages/news-dialog.component.html rename to frontend/app/features/apps/pages/news-dialog.component.html diff --git a/src/Squidex/app/features/apps/pages/news-dialog.component.scss b/frontend/app/features/apps/pages/news-dialog.component.scss similarity index 100% rename from src/Squidex/app/features/apps/pages/news-dialog.component.scss rename to frontend/app/features/apps/pages/news-dialog.component.scss diff --git a/src/Squidex/app/features/apps/pages/news-dialog.component.ts b/frontend/app/features/apps/pages/news-dialog.component.ts similarity index 100% rename from src/Squidex/app/features/apps/pages/news-dialog.component.ts rename to frontend/app/features/apps/pages/news-dialog.component.ts diff --git a/src/Squidex/app/features/apps/pages/onboarding-dialog.component.html b/frontend/app/features/apps/pages/onboarding-dialog.component.html similarity index 100% rename from src/Squidex/app/features/apps/pages/onboarding-dialog.component.html rename to frontend/app/features/apps/pages/onboarding-dialog.component.html diff --git a/src/Squidex/app/features/apps/pages/onboarding-dialog.component.scss b/frontend/app/features/apps/pages/onboarding-dialog.component.scss similarity index 100% rename from src/Squidex/app/features/apps/pages/onboarding-dialog.component.scss rename to frontend/app/features/apps/pages/onboarding-dialog.component.scss diff --git a/src/Squidex/app/features/apps/pages/onboarding-dialog.component.ts b/frontend/app/features/apps/pages/onboarding-dialog.component.ts similarity index 100% rename from src/Squidex/app/features/apps/pages/onboarding-dialog.component.ts rename to frontend/app/features/apps/pages/onboarding-dialog.component.ts diff --git a/src/Squidex/app/features/assets/declarations.ts b/frontend/app/features/assets/declarations.ts similarity index 100% rename from src/Squidex/app/features/assets/declarations.ts rename to frontend/app/features/assets/declarations.ts diff --git a/src/Squidex/app/features/assets/index.ts b/frontend/app/features/assets/index.ts similarity index 100% rename from src/Squidex/app/features/assets/index.ts rename to frontend/app/features/assets/index.ts diff --git a/src/Squidex/app/features/assets/module.ts b/frontend/app/features/assets/module.ts similarity index 100% rename from src/Squidex/app/features/assets/module.ts rename to frontend/app/features/assets/module.ts diff --git a/src/Squidex/app/features/assets/pages/assets-filters-page.component.html b/frontend/app/features/assets/pages/assets-filters-page.component.html similarity index 100% rename from src/Squidex/app/features/assets/pages/assets-filters-page.component.html rename to frontend/app/features/assets/pages/assets-filters-page.component.html diff --git a/src/Squidex/app/features/assets/pages/assets-filters-page.component.scss b/frontend/app/features/assets/pages/assets-filters-page.component.scss similarity index 100% rename from src/Squidex/app/features/assets/pages/assets-filters-page.component.scss rename to frontend/app/features/assets/pages/assets-filters-page.component.scss diff --git a/src/Squidex/app/features/assets/pages/assets-filters-page.component.ts b/frontend/app/features/assets/pages/assets-filters-page.component.ts similarity index 100% rename from src/Squidex/app/features/assets/pages/assets-filters-page.component.ts rename to frontend/app/features/assets/pages/assets-filters-page.component.ts diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.html b/frontend/app/features/assets/pages/assets-page.component.html similarity index 100% rename from src/Squidex/app/features/assets/pages/assets-page.component.html rename to frontend/app/features/assets/pages/assets-page.component.html diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.scss b/frontend/app/features/assets/pages/assets-page.component.scss similarity index 100% rename from src/Squidex/app/features/assets/pages/assets-page.component.scss rename to frontend/app/features/assets/pages/assets-page.component.scss diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.ts b/frontend/app/features/assets/pages/assets-page.component.ts similarity index 100% rename from src/Squidex/app/features/assets/pages/assets-page.component.ts rename to frontend/app/features/assets/pages/assets-page.component.ts diff --git a/src/Squidex/app/features/content/declarations.ts b/frontend/app/features/content/declarations.ts similarity index 100% rename from src/Squidex/app/features/content/declarations.ts rename to frontend/app/features/content/declarations.ts diff --git a/src/Squidex/app/features/content/index.ts b/frontend/app/features/content/index.ts similarity index 100% rename from src/Squidex/app/features/content/index.ts rename to frontend/app/features/content/index.ts diff --git a/src/Squidex/app/features/content/module.ts b/frontend/app/features/content/module.ts similarity index 100% rename from src/Squidex/app/features/content/module.ts rename to frontend/app/features/content/module.ts diff --git a/src/Squidex/app/features/content/pages/comments/comments-page.component.html b/frontend/app/features/content/pages/comments/comments-page.component.html similarity index 100% rename from src/Squidex/app/features/content/pages/comments/comments-page.component.html rename to frontend/app/features/content/pages/comments/comments-page.component.html diff --git a/src/Squidex/app/features/content/pages/comments/comments-page.component.scss b/frontend/app/features/content/pages/comments/comments-page.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/comments/comments-page.component.scss rename to frontend/app/features/content/pages/comments/comments-page.component.scss diff --git a/src/Squidex/app/features/content/pages/comments/comments-page.component.ts b/frontend/app/features/content/pages/comments/comments-page.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/comments/comments-page.component.ts rename to frontend/app/features/content/pages/comments/comments-page.component.ts diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.html b/frontend/app/features/content/pages/content/content-field.component.html similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-field.component.html rename to frontend/app/features/content/pages/content/content-field.component.html diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.scss b/frontend/app/features/content/pages/content/content-field.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-field.component.scss rename to frontend/app/features/content/pages/content/content-field.component.scss diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.ts b/frontend/app/features/content/pages/content/content-field.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-field.component.ts rename to frontend/app/features/content/pages/content/content-field.component.ts diff --git a/src/Squidex/app/features/content/pages/content/content-history-page.component.html b/frontend/app/features/content/pages/content/content-history-page.component.html similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-history-page.component.html rename to frontend/app/features/content/pages/content/content-history-page.component.html diff --git a/src/Squidex/app/features/content/pages/content/content-history-page.component.scss b/frontend/app/features/content/pages/content/content-history-page.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-history-page.component.scss rename to frontend/app/features/content/pages/content/content-history-page.component.scss diff --git a/src/Squidex/app/features/content/pages/content/content-history-page.component.ts b/frontend/app/features/content/pages/content/content-history-page.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-history-page.component.ts rename to frontend/app/features/content/pages/content/content-history-page.component.ts diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/frontend/app/features/content/pages/content/content-page.component.html similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-page.component.html rename to frontend/app/features/content/pages/content/content-page.component.html diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.scss b/frontend/app/features/content/pages/content/content-page.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-page.component.scss rename to frontend/app/features/content/pages/content/content-page.component.scss diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/frontend/app/features/content/pages/content/content-page.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-page.component.ts rename to frontend/app/features/content/pages/content/content-page.component.ts diff --git a/src/Squidex/app/features/content/pages/content/field-languages.component.ts b/frontend/app/features/content/pages/content/field-languages.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/content/field-languages.component.ts rename to frontend/app/features/content/pages/content/field-languages.component.ts diff --git a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html b/frontend/app/features/content/pages/contents/contents-filters-page.component.html similarity index 100% rename from src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html rename to frontend/app/features/content/pages/contents/contents-filters-page.component.html diff --git a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.scss b/frontend/app/features/content/pages/contents/contents-filters-page.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/contents/contents-filters-page.component.scss rename to frontend/app/features/content/pages/contents/contents-filters-page.component.scss diff --git a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts b/frontend/app/features/content/pages/contents/contents-filters-page.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts rename to frontend/app/features/content/pages/contents/contents-filters-page.component.ts diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/frontend/app/features/content/pages/contents/contents-page.component.html similarity index 100% rename from src/Squidex/app/features/content/pages/contents/contents-page.component.html rename to frontend/app/features/content/pages/contents/contents-page.component.html diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.scss b/frontend/app/features/content/pages/contents/contents-page.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/contents/contents-page.component.scss rename to frontend/app/features/content/pages/contents/contents-page.component.scss diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/frontend/app/features/content/pages/contents/contents-page.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/contents/contents-page.component.ts rename to frontend/app/features/content/pages/contents/contents-page.component.ts diff --git a/src/Squidex/app/features/content/pages/messages.ts b/frontend/app/features/content/pages/messages.ts similarity index 100% rename from src/Squidex/app/features/content/pages/messages.ts rename to frontend/app/features/content/pages/messages.ts diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html b/frontend/app/features/content/pages/schemas/schemas-page.component.html similarity index 100% rename from src/Squidex/app/features/content/pages/schemas/schemas-page.component.html rename to frontend/app/features/content/pages/schemas/schemas-page.component.html diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.scss b/frontend/app/features/content/pages/schemas/schemas-page.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/schemas/schemas-page.component.scss rename to frontend/app/features/content/pages/schemas/schemas-page.component.scss diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts b/frontend/app/features/content/pages/schemas/schemas-page.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts rename to frontend/app/features/content/pages/schemas/schemas-page.component.ts diff --git a/src/Squidex/app/features/content/shared/array-editor.component.html b/frontend/app/features/content/shared/array-editor.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/array-editor.component.html rename to frontend/app/features/content/shared/array-editor.component.html diff --git a/src/Squidex/app/features/content/shared/array-editor.component.scss b/frontend/app/features/content/shared/array-editor.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/array-editor.component.scss rename to frontend/app/features/content/shared/array-editor.component.scss diff --git a/src/Squidex/app/features/content/shared/array-editor.component.ts b/frontend/app/features/content/shared/array-editor.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/array-editor.component.ts rename to frontend/app/features/content/shared/array-editor.component.ts diff --git a/src/Squidex/app/features/content/shared/array-item.component.html b/frontend/app/features/content/shared/array-item.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/array-item.component.html rename to frontend/app/features/content/shared/array-item.component.html diff --git a/src/Squidex/app/features/content/shared/array-item.component.scss b/frontend/app/features/content/shared/array-item.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/array-item.component.scss rename to frontend/app/features/content/shared/array-item.component.scss diff --git a/src/Squidex/app/features/content/shared/array-item.component.ts b/frontend/app/features/content/shared/array-item.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/array-item.component.ts rename to frontend/app/features/content/shared/array-item.component.ts diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.html b/frontend/app/features/content/shared/assets-editor.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/assets-editor.component.html rename to frontend/app/features/content/shared/assets-editor.component.html diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.scss b/frontend/app/features/content/shared/assets-editor.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/assets-editor.component.scss rename to frontend/app/features/content/shared/assets-editor.component.scss diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.ts b/frontend/app/features/content/shared/assets-editor.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/assets-editor.component.ts rename to frontend/app/features/content/shared/assets-editor.component.ts diff --git a/src/Squidex/app/features/content/shared/content-selector-item.component.ts b/frontend/app/features/content/shared/content-selector-item.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/content-selector-item.component.ts rename to frontend/app/features/content/shared/content-selector-item.component.ts diff --git a/src/Squidex/app/features/content/shared/content-status.component.html b/frontend/app/features/content/shared/content-status.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/content-status.component.html rename to frontend/app/features/content/shared/content-status.component.html diff --git a/src/Squidex/app/features/content/shared/content-status.component.scss b/frontend/app/features/content/shared/content-status.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/content-status.component.scss rename to frontend/app/features/content/shared/content-status.component.scss diff --git a/src/Squidex/app/features/content/shared/content-status.component.ts b/frontend/app/features/content/shared/content-status.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/content-status.component.ts rename to frontend/app/features/content/shared/content-status.component.ts diff --git a/src/Squidex/app/features/content/shared/content-value-editor.component.ts b/frontend/app/features/content/shared/content-value-editor.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/content-value-editor.component.ts rename to frontend/app/features/content/shared/content-value-editor.component.ts diff --git a/src/Squidex/app/features/content/shared/content-value.component.ts b/frontend/app/features/content/shared/content-value.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/content-value.component.ts rename to frontend/app/features/content/shared/content-value.component.ts diff --git a/src/Squidex/app/features/content/shared/content.component.html b/frontend/app/features/content/shared/content.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/content.component.html rename to frontend/app/features/content/shared/content.component.html diff --git a/src/Squidex/app/features/content/shared/content.component.scss b/frontend/app/features/content/shared/content.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/content.component.scss rename to frontend/app/features/content/shared/content.component.scss diff --git a/src/Squidex/app/features/content/shared/content.component.ts b/frontend/app/features/content/shared/content.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/content.component.ts rename to frontend/app/features/content/shared/content.component.ts diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.html b/frontend/app/features/content/shared/contents-selector.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/contents-selector.component.html rename to frontend/app/features/content/shared/contents-selector.component.html diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.scss b/frontend/app/features/content/shared/contents-selector.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/contents-selector.component.scss rename to frontend/app/features/content/shared/contents-selector.component.scss diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.ts b/frontend/app/features/content/shared/contents-selector.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/contents-selector.component.ts rename to frontend/app/features/content/shared/contents-selector.component.ts diff --git a/src/Squidex/app/features/content/shared/due-time-selector.component.html b/frontend/app/features/content/shared/due-time-selector.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/due-time-selector.component.html rename to frontend/app/features/content/shared/due-time-selector.component.html diff --git a/src/Squidex/app/features/content/shared/due-time-selector.component.scss b/frontend/app/features/content/shared/due-time-selector.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/due-time-selector.component.scss rename to frontend/app/features/content/shared/due-time-selector.component.scss diff --git a/src/Squidex/app/features/content/shared/due-time-selector.component.ts b/frontend/app/features/content/shared/due-time-selector.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/due-time-selector.component.ts rename to frontend/app/features/content/shared/due-time-selector.component.ts diff --git a/src/Squidex/app/features/content/shared/field-editor.component.html b/frontend/app/features/content/shared/field-editor.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/field-editor.component.html rename to frontend/app/features/content/shared/field-editor.component.html diff --git a/src/Squidex/app/features/content/shared/field-editor.component.scss b/frontend/app/features/content/shared/field-editor.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/field-editor.component.scss rename to frontend/app/features/content/shared/field-editor.component.scss diff --git a/src/Squidex/app/features/content/shared/field-editor.component.ts b/frontend/app/features/content/shared/field-editor.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/field-editor.component.ts rename to frontend/app/features/content/shared/field-editor.component.ts diff --git a/src/Squidex/app/features/content/shared/preview-button.component.html b/frontend/app/features/content/shared/preview-button.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/preview-button.component.html rename to frontend/app/features/content/shared/preview-button.component.html diff --git a/src/Squidex/app/features/content/shared/preview-button.component.scss b/frontend/app/features/content/shared/preview-button.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/preview-button.component.scss rename to frontend/app/features/content/shared/preview-button.component.scss diff --git a/src/Squidex/app/features/content/shared/preview-button.component.ts b/frontend/app/features/content/shared/preview-button.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/preview-button.component.ts rename to frontend/app/features/content/shared/preview-button.component.ts diff --git a/src/Squidex/app/features/content/shared/reference-item.component.scss b/frontend/app/features/content/shared/reference-item.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/reference-item.component.scss rename to frontend/app/features/content/shared/reference-item.component.scss diff --git a/src/Squidex/app/features/content/shared/reference-item.component.ts b/frontend/app/features/content/shared/reference-item.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/reference-item.component.ts rename to frontend/app/features/content/shared/reference-item.component.ts diff --git a/src/Squidex/app/features/content/shared/references-editor.component.html b/frontend/app/features/content/shared/references-editor.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/references-editor.component.html rename to frontend/app/features/content/shared/references-editor.component.html diff --git a/src/Squidex/app/features/content/shared/references-editor.component.scss b/frontend/app/features/content/shared/references-editor.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/references-editor.component.scss rename to frontend/app/features/content/shared/references-editor.component.scss diff --git a/src/Squidex/app/features/content/shared/references-editor.component.ts b/frontend/app/features/content/shared/references-editor.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/references-editor.component.ts rename to frontend/app/features/content/shared/references-editor.component.ts diff --git a/src/Squidex/app/features/dashboard/declarations.ts b/frontend/app/features/dashboard/declarations.ts similarity index 100% rename from src/Squidex/app/features/dashboard/declarations.ts rename to frontend/app/features/dashboard/declarations.ts diff --git a/src/Squidex/app/features/dashboard/index.ts b/frontend/app/features/dashboard/index.ts similarity index 100% rename from src/Squidex/app/features/dashboard/index.ts rename to frontend/app/features/dashboard/index.ts diff --git a/src/Squidex/app/features/dashboard/module.ts b/frontend/app/features/dashboard/module.ts similarity index 100% rename from src/Squidex/app/features/dashboard/module.ts rename to frontend/app/features/dashboard/module.ts diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.html b/frontend/app/features/dashboard/pages/dashboard-page.component.html similarity index 100% rename from src/Squidex/app/features/dashboard/pages/dashboard-page.component.html rename to frontend/app/features/dashboard/pages/dashboard-page.component.html diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.scss b/frontend/app/features/dashboard/pages/dashboard-page.component.scss similarity index 100% rename from src/Squidex/app/features/dashboard/pages/dashboard-page.component.scss rename to frontend/app/features/dashboard/pages/dashboard-page.component.scss diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts b/frontend/app/features/dashboard/pages/dashboard-page.component.ts similarity index 100% rename from src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts rename to frontend/app/features/dashboard/pages/dashboard-page.component.ts diff --git a/src/Squidex/app/features/rules/declarations.ts b/frontend/app/features/rules/declarations.ts similarity index 100% rename from src/Squidex/app/features/rules/declarations.ts rename to frontend/app/features/rules/declarations.ts diff --git a/src/Squidex/app/features/rules/index.ts b/frontend/app/features/rules/index.ts similarity index 100% rename from src/Squidex/app/features/rules/index.ts rename to frontend/app/features/rules/index.ts diff --git a/src/Squidex/app/features/rules/module.ts b/frontend/app/features/rules/module.ts similarity index 100% rename from src/Squidex/app/features/rules/module.ts rename to frontend/app/features/rules/module.ts diff --git a/src/Squidex/app/features/rules/pages/events/pipes.ts b/frontend/app/features/rules/pages/events/pipes.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/events/pipes.ts rename to frontend/app/features/rules/pages/events/pipes.ts diff --git a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html b/frontend/app/features/rules/pages/events/rule-events-page.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/events/rule-events-page.component.html rename to frontend/app/features/rules/pages/events/rule-events-page.component.html diff --git a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss b/frontend/app/features/rules/pages/events/rule-events-page.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss rename to frontend/app/features/rules/pages/events/rule-events-page.component.scss diff --git a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts b/frontend/app/features/rules/pages/events/rule-events-page.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts rename to frontend/app/features/rules/pages/events/rule-events-page.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.html b/frontend/app/features/rules/pages/rules/actions/generic-action.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.html rename to frontend/app/features/rules/pages/rules/actions/generic-action.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.scss b/frontend/app/features/rules/pages/rules/actions/generic-action.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.scss rename to frontend/app/features/rules/pages/rules/actions/generic-action.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.ts b/frontend/app/features/rules/pages/rules/actions/generic-action.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.ts rename to frontend/app/features/rules/pages/rules/actions/generic-action.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/rule-element.component.html b/frontend/app/features/rules/pages/rules/rule-element.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule-element.component.html rename to frontend/app/features/rules/pages/rules/rule-element.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/rule-element.component.scss b/frontend/app/features/rules/pages/rules/rule-element.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule-element.component.scss rename to frontend/app/features/rules/pages/rules/rule-element.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/rule-element.component.ts b/frontend/app/features/rules/pages/rules/rule-element.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule-element.component.ts rename to frontend/app/features/rules/pages/rules/rule-element.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/rule-icon.component.ts b/frontend/app/features/rules/pages/rules/rule-icon.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule-icon.component.ts rename to frontend/app/features/rules/pages/rules/rule-icon.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html b/frontend/app/features/rules/pages/rules/rule-wizard.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html rename to frontend/app/features/rules/pages/rules/rule-wizard.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss b/frontend/app/features/rules/pages/rules/rule-wizard.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss rename to frontend/app/features/rules/pages/rules/rule-wizard.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts b/frontend/app/features/rules/pages/rules/rule-wizard.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts rename to frontend/app/features/rules/pages/rules/rule-wizard.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/rule.component.html b/frontend/app/features/rules/pages/rules/rule.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule.component.html rename to frontend/app/features/rules/pages/rules/rule.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/rule.component.scss b/frontend/app/features/rules/pages/rules/rule.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule.component.scss rename to frontend/app/features/rules/pages/rules/rule.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/rule.component.ts b/frontend/app/features/rules/pages/rules/rule.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule.component.ts rename to frontend/app/features/rules/pages/rules/rule.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.html b/frontend/app/features/rules/pages/rules/rules-page.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rules-page.component.html rename to frontend/app/features/rules/pages/rules/rules-page.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.scss b/frontend/app/features/rules/pages/rules/rules-page.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rules-page.component.scss rename to frontend/app/features/rules/pages/rules/rules-page.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts b/frontend/app/features/rules/pages/rules/rules-page.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rules-page.component.ts rename to frontend/app/features/rules/pages/rules/rules-page.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html b/frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html rename to frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.scss b/frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.scss rename to frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts b/frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts rename to frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html b/frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html rename to frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss b/frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss rename to frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts b/frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts rename to frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.html b/frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.html rename to frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.scss b/frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.scss rename to frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts b/frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts rename to frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html b/frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html rename to frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.scss b/frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.scss rename to frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts b/frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts rename to frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.ts diff --git a/src/Squidex/app/features/schemas/declarations.ts b/frontend/app/features/schemas/declarations.ts similarity index 100% rename from src/Squidex/app/features/schemas/declarations.ts rename to frontend/app/features/schemas/declarations.ts diff --git a/src/Squidex/app/features/schemas/index.ts b/frontend/app/features/schemas/index.ts similarity index 100% rename from src/Squidex/app/features/schemas/index.ts rename to frontend/app/features/schemas/index.ts diff --git a/src/Squidex/app/features/schemas/module.ts b/frontend/app/features/schemas/module.ts similarity index 100% rename from src/Squidex/app/features/schemas/module.ts rename to frontend/app/features/schemas/module.ts diff --git a/src/Squidex/app/features/schemas/pages/messages.ts b/frontend/app/features/schemas/pages/messages.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/messages.ts rename to frontend/app/features/schemas/pages/messages.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html b/frontend/app/features/schemas/pages/schema/field-wizard.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html rename to frontend/app/features/schemas/pages/schema/field-wizard.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.scss b/frontend/app/features/schemas/pages/schema/field-wizard.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/field-wizard.component.scss rename to frontend/app/features/schemas/pages/schema/field-wizard.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts b/frontend/app/features/schemas/pages/schema/field-wizard.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts rename to frontend/app/features/schemas/pages/schema/field-wizard.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.html b/frontend/app/features/schemas/pages/schema/field.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/field.component.html rename to frontend/app/features/schemas/pages/schema/field.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.scss b/frontend/app/features/schemas/pages/schema/field.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/field.component.scss rename to frontend/app/features/schemas/pages/schema/field.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.ts b/frontend/app/features/schemas/pages/schema/field.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/field.component.ts rename to frontend/app/features/schemas/pages/schema/field.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.ts b/frontend/app/features/schemas/pages/schema/forms/field-form-common.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.ts rename to frontend/app/features/schemas/pages/schema/forms/field-form-common.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.ts b/frontend/app/features/schemas/pages/schema/forms/field-form-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.ts rename to frontend/app/features/schemas/pages/schema/forms/field-form-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.ts b/frontend/app/features/schemas/pages/schema/forms/field-form-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.ts rename to frontend/app/features/schemas/pages/schema/forms/field-form-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/forms/field-form.component.ts b/frontend/app/features/schemas/pages/schema/forms/field-form.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/forms/field-form.component.ts rename to frontend/app/features/schemas/pages/schema/forms/field-form.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.html b/frontend/app/features/schemas/pages/schema/schema-edit-form.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.html rename to frontend/app/features/schemas/pages/schema/schema-edit-form.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.scss b/frontend/app/features/schemas/pages/schema/schema-edit-form.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.scss rename to frontend/app/features/schemas/pages/schema/schema-edit-form.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts b/frontend/app/features/schemas/pages/schema/schema-edit-form.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts rename to frontend/app/features/schemas/pages/schema/schema-edit-form.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html b/frontend/app/features/schemas/pages/schema/schema-export-form.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html rename to frontend/app/features/schemas/pages/schema/schema-export-form.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss b/frontend/app/features/schemas/pages/schema/schema-export-form.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss rename to frontend/app/features/schemas/pages/schema/schema-export-form.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.ts b/frontend/app/features/schemas/pages/schema/schema-export-form.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.ts rename to frontend/app/features/schemas/pages/schema/schema-export-form.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/frontend/app/features/schemas/pages/schema/schema-page.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-page.component.html rename to frontend/app/features/schemas/pages/schema/schema-page.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss b/frontend/app/features/schemas/pages/schema/schema-page.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss rename to frontend/app/features/schemas/pages/schema/schema-page.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/frontend/app/features/schemas/pages/schema/schema-page.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts rename to frontend/app/features/schemas/pages/schema/schema-page.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.html b/frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.html rename to frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.scss b/frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.scss rename to frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts b/frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts rename to frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.html b/frontend/app/features/schemas/pages/schema/schema-scripts-form.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.html rename to frontend/app/features/schemas/pages/schema/schema-scripts-form.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.scss b/frontend/app/features/schemas/pages/schema/schema-scripts-form.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.scss rename to frontend/app/features/schemas/pages/schema/schema-scripts-form.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts b/frontend/app/features/schemas/pages/schema/schema-scripts-form.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts rename to frontend/app/features/schemas/pages/schema/schema-scripts-form.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/array-validation.component.html b/frontend/app/features/schemas/pages/schema/types/array-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/array-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/array-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/array-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/array-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/array-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/array-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/array-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/array-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/array-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/array-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.html b/frontend/app/features/schemas/pages/schema/types/assets-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/assets-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/assets-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/assets-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/assets-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/assets-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.html b/frontend/app/features/schemas/pages/schema/types/assets-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/assets-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/assets-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/assets-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/assets-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/assets-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html b/frontend/app/features/schemas/pages/schema/types/boolean-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/boolean-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/boolean-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/boolean-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/boolean-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/boolean-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html b/frontend/app/features/schemas/pages/schema/types/boolean-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/boolean-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/boolean-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/boolean-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/boolean-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/boolean-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.html b/frontend/app/features/schemas/pages/schema/types/date-time-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/date-time-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/date-time-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/date-time-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/date-time-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/date-time-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.html b/frontend/app/features/schemas/pages/schema/types/date-time-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/date-time-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/date-time-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/date-time-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/date-time-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/date-time-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.html b/frontend/app/features/schemas/pages/schema/types/geolocation-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/geolocation-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/geolocation-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/geolocation-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/geolocation-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/geolocation-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.html b/frontend/app/features/schemas/pages/schema/types/geolocation-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/geolocation-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/geolocation-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/geolocation-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/geolocation-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/geolocation-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.html b/frontend/app/features/schemas/pages/schema/types/json-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/json-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/json-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/json-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/json-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/json-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/json-validation.component.html b/frontend/app/features/schemas/pages/schema/types/json-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/json-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/json-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/json-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/json-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/json-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/json-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/json-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/json-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/json-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/json-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html b/frontend/app/features/schemas/pages/schema/types/number-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/number-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/number-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/number-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/number-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/number-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.html b/frontend/app/features/schemas/pages/schema/types/number-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/number-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/number-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/number-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/number-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/number-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.html b/frontend/app/features/schemas/pages/schema/types/references-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/references-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/references-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/references-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/references-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/references-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html b/frontend/app/features/schemas/pages/schema/types/references-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/references-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/references-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/references-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/references-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/references-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html b/frontend/app/features/schemas/pages/schema/types/string-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/string-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/string-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/string-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/string-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/string-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html b/frontend/app/features/schemas/pages/schema/types/string-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/string-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/string-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/string-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/string-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/string-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.html b/frontend/app/features/schemas/pages/schema/types/tags-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/tags-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/tags-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/tags-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/tags-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/tags-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/tags-validation.component.html b/frontend/app/features/schemas/pages/schema/types/tags-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/tags-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/tags-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/tags-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/tags-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/tags-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/tags-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/tags-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/tags-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/tags-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/tags-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html b/frontend/app/features/schemas/pages/schemas/schema-form.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html rename to frontend/app/features/schemas/pages/schemas/schema-form.component.html diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.scss b/frontend/app/features/schemas/pages/schemas/schema-form.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schemas/schema-form.component.scss rename to frontend/app/features/schemas/pages/schemas/schema-form.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts b/frontend/app/features/schemas/pages/schemas/schema-form.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts rename to frontend/app/features/schemas/pages/schemas/schema-form.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html b/frontend/app/features/schemas/pages/schemas/schemas-page.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html rename to frontend/app/features/schemas/pages/schemas/schemas-page.component.html diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss b/frontend/app/features/schemas/pages/schemas/schemas-page.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss rename to frontend/app/features/schemas/pages/schemas/schemas-page.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts b/frontend/app/features/schemas/pages/schemas/schemas-page.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts rename to frontend/app/features/schemas/pages/schemas/schemas-page.component.ts diff --git a/src/Squidex/app/features/settings/declarations.ts b/frontend/app/features/settings/declarations.ts similarity index 100% rename from src/Squidex/app/features/settings/declarations.ts rename to frontend/app/features/settings/declarations.ts diff --git a/src/Squidex/app/features/settings/index.ts b/frontend/app/features/settings/index.ts similarity index 100% rename from src/Squidex/app/features/settings/index.ts rename to frontend/app/features/settings/index.ts diff --git a/src/Squidex/app/features/settings/module.ts b/frontend/app/features/settings/module.ts similarity index 100% rename from src/Squidex/app/features/settings/module.ts rename to frontend/app/features/settings/module.ts diff --git a/src/Squidex/app/features/settings/pages/backups/backup.component.ts b/frontend/app/features/settings/pages/backups/backup.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/backups/backup.component.ts rename to frontend/app/features/settings/pages/backups/backup.component.ts diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html b/frontend/app/features/settings/pages/backups/backups-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/backups/backups-page.component.html rename to frontend/app/features/settings/pages/backups/backups-page.component.html diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.scss b/frontend/app/features/settings/pages/backups/backups-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/backups/backups-page.component.scss rename to frontend/app/features/settings/pages/backups/backups-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts b/frontend/app/features/settings/pages/backups/backups-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/backups/backups-page.component.ts rename to frontend/app/features/settings/pages/backups/backups-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/clients/client-add-form.component.ts b/frontend/app/features/settings/pages/clients/client-add-form.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/clients/client-add-form.component.ts rename to frontend/app/features/settings/pages/clients/client-add-form.component.ts diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.html b/frontend/app/features/settings/pages/clients/client.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/clients/client.component.html rename to frontend/app/features/settings/pages/clients/client.component.html diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.scss b/frontend/app/features/settings/pages/clients/client.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/clients/client.component.scss rename to frontend/app/features/settings/pages/clients/client.component.scss diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.ts b/frontend/app/features/settings/pages/clients/client.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/clients/client.component.ts rename to frontend/app/features/settings/pages/clients/client.component.ts diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.html b/frontend/app/features/settings/pages/clients/clients-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/clients/clients-page.component.html rename to frontend/app/features/settings/pages/clients/clients-page.component.html diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.scss b/frontend/app/features/settings/pages/clients/clients-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/clients/clients-page.component.scss rename to frontend/app/features/settings/pages/clients/clients-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts b/frontend/app/features/settings/pages/clients/clients-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/clients/clients-page.component.ts rename to frontend/app/features/settings/pages/clients/clients-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/contributors/contributor-add-form.component.html b/frontend/app/features/settings/pages/contributors/contributor-add-form.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/contributor-add-form.component.html rename to frontend/app/features/settings/pages/contributors/contributor-add-form.component.html diff --git a/src/Squidex/app/features/settings/pages/contributors/contributor-add-form.component.scss b/frontend/app/features/settings/pages/contributors/contributor-add-form.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/contributor-add-form.component.scss rename to frontend/app/features/settings/pages/contributors/contributor-add-form.component.scss diff --git a/src/Squidex/app/features/settings/pages/contributors/contributor-add-form.component.ts b/frontend/app/features/settings/pages/contributors/contributor-add-form.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/contributor-add-form.component.ts rename to frontend/app/features/settings/pages/contributors/contributor-add-form.component.ts diff --git a/src/Squidex/app/features/settings/pages/contributors/contributor.component.ts b/frontend/app/features/settings/pages/contributors/contributor.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/contributor.component.ts rename to frontend/app/features/settings/pages/contributors/contributor.component.ts diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html b/frontend/app/features/settings/pages/contributors/contributors-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html rename to frontend/app/features/settings/pages/contributors/contributors-page.component.html diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss b/frontend/app/features/settings/pages/contributors/contributors-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss rename to frontend/app/features/settings/pages/contributors/contributors-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts b/frontend/app/features/settings/pages/contributors/contributors-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts rename to frontend/app/features/settings/pages/contributors/contributors-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.html b/frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.html rename to frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.html diff --git a/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.scss b/frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.scss rename to frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.scss diff --git a/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.ts b/frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.ts rename to frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.ts diff --git a/src/Squidex/app/features/settings/pages/languages/language-add-form.component.ts b/frontend/app/features/settings/pages/languages/language-add-form.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/languages/language-add-form.component.ts rename to frontend/app/features/settings/pages/languages/language-add-form.component.ts diff --git a/src/Squidex/app/features/settings/pages/languages/language.component.html b/frontend/app/features/settings/pages/languages/language.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/languages/language.component.html rename to frontend/app/features/settings/pages/languages/language.component.html diff --git a/src/Squidex/app/features/settings/pages/languages/language.component.scss b/frontend/app/features/settings/pages/languages/language.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/languages/language.component.scss rename to frontend/app/features/settings/pages/languages/language.component.scss diff --git a/src/Squidex/app/features/settings/pages/languages/language.component.ts b/frontend/app/features/settings/pages/languages/language.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/languages/language.component.ts rename to frontend/app/features/settings/pages/languages/language.component.ts diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.html b/frontend/app/features/settings/pages/languages/languages-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/languages/languages-page.component.html rename to frontend/app/features/settings/pages/languages/languages-page.component.html diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.scss b/frontend/app/features/settings/pages/languages/languages-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/languages/languages-page.component.scss rename to frontend/app/features/settings/pages/languages/languages-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts b/frontend/app/features/settings/pages/languages/languages-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/languages/languages-page.component.ts rename to frontend/app/features/settings/pages/languages/languages-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/more/more-page.component.html b/frontend/app/features/settings/pages/more/more-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/more/more-page.component.html rename to frontend/app/features/settings/pages/more/more-page.component.html diff --git a/src/Squidex/app/features/settings/pages/more/more-page.component.scss b/frontend/app/features/settings/pages/more/more-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/more/more-page.component.scss rename to frontend/app/features/settings/pages/more/more-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/more/more-page.component.ts b/frontend/app/features/settings/pages/more/more-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/more/more-page.component.ts rename to frontend/app/features/settings/pages/more/more-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/patterns/pattern.component.html b/frontend/app/features/settings/pages/patterns/pattern.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/patterns/pattern.component.html rename to frontend/app/features/settings/pages/patterns/pattern.component.html diff --git a/src/Squidex/app/features/settings/pages/patterns/pattern.component.scss b/frontend/app/features/settings/pages/patterns/pattern.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/patterns/pattern.component.scss rename to frontend/app/features/settings/pages/patterns/pattern.component.scss diff --git a/src/Squidex/app/features/settings/pages/patterns/pattern.component.ts b/frontend/app/features/settings/pages/patterns/pattern.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/patterns/pattern.component.ts rename to frontend/app/features/settings/pages/patterns/pattern.component.ts diff --git a/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html b/frontend/app/features/settings/pages/patterns/patterns-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html rename to frontend/app/features/settings/pages/patterns/patterns-page.component.html diff --git a/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.scss b/frontend/app/features/settings/pages/patterns/patterns-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/patterns/patterns-page.component.scss rename to frontend/app/features/settings/pages/patterns/patterns-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.ts b/frontend/app/features/settings/pages/patterns/patterns-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/patterns/patterns-page.component.ts rename to frontend/app/features/settings/pages/patterns/patterns-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/plans/plan.component.html b/frontend/app/features/settings/pages/plans/plan.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/plans/plan.component.html rename to frontend/app/features/settings/pages/plans/plan.component.html diff --git a/src/Squidex/app/features/settings/pages/plans/plan.component.scss b/frontend/app/features/settings/pages/plans/plan.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/plans/plan.component.scss rename to frontend/app/features/settings/pages/plans/plan.component.scss diff --git a/src/Squidex/app/features/settings/pages/plans/plan.component.ts b/frontend/app/features/settings/pages/plans/plan.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/plans/plan.component.ts rename to frontend/app/features/settings/pages/plans/plan.component.ts diff --git a/src/Squidex/app/features/settings/pages/plans/plans-page.component.html b/frontend/app/features/settings/pages/plans/plans-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/plans/plans-page.component.html rename to frontend/app/features/settings/pages/plans/plans-page.component.html diff --git a/src/Squidex/app/features/settings/pages/plans/plans-page.component.scss b/frontend/app/features/settings/pages/plans/plans-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/plans/plans-page.component.scss rename to frontend/app/features/settings/pages/plans/plans-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/plans/plans-page.component.ts b/frontend/app/features/settings/pages/plans/plans-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/plans/plans-page.component.ts rename to frontend/app/features/settings/pages/plans/plans-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/roles/role-add-form.component.ts b/frontend/app/features/settings/pages/roles/role-add-form.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/roles/role-add-form.component.ts rename to frontend/app/features/settings/pages/roles/role-add-form.component.ts diff --git a/src/Squidex/app/features/settings/pages/roles/role.component.html b/frontend/app/features/settings/pages/roles/role.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/roles/role.component.html rename to frontend/app/features/settings/pages/roles/role.component.html diff --git a/src/Squidex/app/features/settings/pages/roles/role.component.scss b/frontend/app/features/settings/pages/roles/role.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/roles/role.component.scss rename to frontend/app/features/settings/pages/roles/role.component.scss diff --git a/src/Squidex/app/features/settings/pages/roles/role.component.ts b/frontend/app/features/settings/pages/roles/role.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/roles/role.component.ts rename to frontend/app/features/settings/pages/roles/role.component.ts diff --git a/src/Squidex/app/features/settings/pages/roles/roles-page.component.html b/frontend/app/features/settings/pages/roles/roles-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/roles/roles-page.component.html rename to frontend/app/features/settings/pages/roles/roles-page.component.html diff --git a/src/Squidex/app/features/settings/pages/roles/roles-page.component.scss b/frontend/app/features/settings/pages/roles/roles-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/roles/roles-page.component.scss rename to frontend/app/features/settings/pages/roles/roles-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/roles/roles-page.component.ts b/frontend/app/features/settings/pages/roles/roles-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/roles/roles-page.component.ts rename to frontend/app/features/settings/pages/roles/roles-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-add-form.component.ts b/frontend/app/features/settings/pages/workflows/workflow-add-form.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow-add-form.component.ts rename to frontend/app/features/settings/pages/workflows/workflow-add-form.component.ts diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html b/frontend/app/features/settings/pages/workflows/workflow-step.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html rename to frontend/app/features/settings/pages/workflows/workflow-step.component.html diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.scss b/frontend/app/features/settings/pages/workflows/workflow-step.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow-step.component.scss rename to frontend/app/features/settings/pages/workflows/workflow-step.component.scss diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts b/frontend/app/features/settings/pages/workflows/workflow-step.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts rename to frontend/app/features/settings/pages/workflows/workflow-step.component.ts diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-transition.component.html b/frontend/app/features/settings/pages/workflows/workflow-transition.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow-transition.component.html rename to frontend/app/features/settings/pages/workflows/workflow-transition.component.html diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-transition.component.scss b/frontend/app/features/settings/pages/workflows/workflow-transition.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow-transition.component.scss rename to frontend/app/features/settings/pages/workflows/workflow-transition.component.scss diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-transition.component.ts b/frontend/app/features/settings/pages/workflows/workflow-transition.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow-transition.component.ts rename to frontend/app/features/settings/pages/workflows/workflow-transition.component.ts diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.html b/frontend/app/features/settings/pages/workflows/workflow.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow.component.html rename to frontend/app/features/settings/pages/workflows/workflow.component.html diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.scss b/frontend/app/features/settings/pages/workflows/workflow.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow.component.scss rename to frontend/app/features/settings/pages/workflows/workflow.component.scss diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.ts b/frontend/app/features/settings/pages/workflows/workflow.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow.component.ts rename to frontend/app/features/settings/pages/workflows/workflow.component.ts diff --git a/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html b/frontend/app/features/settings/pages/workflows/workflows-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html rename to frontend/app/features/settings/pages/workflows/workflows-page.component.html diff --git a/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.scss b/frontend/app/features/settings/pages/workflows/workflows-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflows-page.component.scss rename to frontend/app/features/settings/pages/workflows/workflows-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.ts b/frontend/app/features/settings/pages/workflows/workflows-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflows-page.component.ts rename to frontend/app/features/settings/pages/workflows/workflows-page.component.ts diff --git a/src/Squidex/app/features/settings/settings-area.component.html b/frontend/app/features/settings/settings-area.component.html similarity index 100% rename from src/Squidex/app/features/settings/settings-area.component.html rename to frontend/app/features/settings/settings-area.component.html diff --git a/src/Squidex/app/features/settings/settings-area.component.scss b/frontend/app/features/settings/settings-area.component.scss similarity index 100% rename from src/Squidex/app/features/settings/settings-area.component.scss rename to frontend/app/features/settings/settings-area.component.scss diff --git a/src/Squidex/app/features/settings/settings-area.component.ts b/frontend/app/features/settings/settings-area.component.ts similarity index 100% rename from src/Squidex/app/features/settings/settings-area.component.ts rename to frontend/app/features/settings/settings-area.component.ts diff --git a/src/Squidex/app/framework/angular/animations.ts b/frontend/app/framework/angular/animations.ts similarity index 100% rename from src/Squidex/app/framework/angular/animations.ts rename to frontend/app/framework/angular/animations.ts diff --git a/src/Squidex/app/framework/angular/avatar.component.ts b/frontend/app/framework/angular/avatar.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/avatar.component.ts rename to frontend/app/framework/angular/avatar.component.ts diff --git a/src/Squidex/app/framework/angular/code.component.html b/frontend/app/framework/angular/code.component.html similarity index 100% rename from src/Squidex/app/framework/angular/code.component.html rename to frontend/app/framework/angular/code.component.html diff --git a/src/Squidex/app/framework/angular/code.component.scss b/frontend/app/framework/angular/code.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/code.component.scss rename to frontend/app/framework/angular/code.component.scss diff --git a/src/Squidex/app/framework/angular/code.component.ts b/frontend/app/framework/angular/code.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/code.component.ts rename to frontend/app/framework/angular/code.component.ts diff --git a/src/Squidex/app/framework/angular/drag-helper.ts b/frontend/app/framework/angular/drag-helper.ts similarity index 100% rename from src/Squidex/app/framework/angular/drag-helper.ts rename to frontend/app/framework/angular/drag-helper.ts diff --git a/src/Squidex/app/framework/angular/external-link.directive.ts b/frontend/app/framework/angular/external-link.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/external-link.directive.ts rename to frontend/app/framework/angular/external-link.directive.ts diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.html b/frontend/app/framework/angular/forms/autocomplete.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/autocomplete.component.html rename to frontend/app/framework/angular/forms/autocomplete.component.html diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.scss b/frontend/app/framework/angular/forms/autocomplete.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/autocomplete.component.scss rename to frontend/app/framework/angular/forms/autocomplete.component.scss diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts b/frontend/app/framework/angular/forms/autocomplete.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/autocomplete.component.ts rename to frontend/app/framework/angular/forms/autocomplete.component.ts diff --git a/src/Squidex/app/framework/angular/forms/checkbox-group.component.html b/frontend/app/framework/angular/forms/checkbox-group.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/checkbox-group.component.html rename to frontend/app/framework/angular/forms/checkbox-group.component.html diff --git a/src/Squidex/app/framework/angular/forms/checkbox-group.component.scss b/frontend/app/framework/angular/forms/checkbox-group.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/checkbox-group.component.scss rename to frontend/app/framework/angular/forms/checkbox-group.component.scss diff --git a/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts b/frontend/app/framework/angular/forms/checkbox-group.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/checkbox-group.component.ts rename to frontend/app/framework/angular/forms/checkbox-group.component.ts diff --git a/src/Squidex/app/framework/angular/forms/code-editor.component.html b/frontend/app/framework/angular/forms/code-editor.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/code-editor.component.html rename to frontend/app/framework/angular/forms/code-editor.component.html diff --git a/src/Squidex/app/framework/angular/forms/code-editor.component.scss b/frontend/app/framework/angular/forms/code-editor.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/code-editor.component.scss rename to frontend/app/framework/angular/forms/code-editor.component.scss diff --git a/src/Squidex/app/framework/angular/forms/code-editor.component.ts b/frontend/app/framework/angular/forms/code-editor.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/code-editor.component.ts rename to frontend/app/framework/angular/forms/code-editor.component.ts diff --git a/src/Squidex/app/framework/angular/forms/color-picker.component.html b/frontend/app/framework/angular/forms/color-picker.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/color-picker.component.html rename to frontend/app/framework/angular/forms/color-picker.component.html diff --git a/src/Squidex/app/framework/angular/forms/color-picker.component.scss b/frontend/app/framework/angular/forms/color-picker.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/color-picker.component.scss rename to frontend/app/framework/angular/forms/color-picker.component.scss diff --git a/src/Squidex/app/framework/angular/forms/color-picker.component.ts b/frontend/app/framework/angular/forms/color-picker.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/color-picker.component.ts rename to frontend/app/framework/angular/forms/color-picker.component.ts diff --git a/src/Squidex/app/framework/angular/forms/confirm-click.directive.ts b/frontend/app/framework/angular/forms/confirm-click.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/confirm-click.directive.ts rename to frontend/app/framework/angular/forms/confirm-click.directive.ts diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.html b/frontend/app/framework/angular/forms/control-errors.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/control-errors.component.html rename to frontend/app/framework/angular/forms/control-errors.component.html diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.scss b/frontend/app/framework/angular/forms/control-errors.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/control-errors.component.scss rename to frontend/app/framework/angular/forms/control-errors.component.scss diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.ts b/frontend/app/framework/angular/forms/control-errors.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/control-errors.component.ts rename to frontend/app/framework/angular/forms/control-errors.component.ts diff --git a/src/Squidex/app/framework/angular/forms/copy.directive.ts b/frontend/app/framework/angular/forms/copy.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/copy.directive.ts rename to frontend/app/framework/angular/forms/copy.directive.ts diff --git a/src/Squidex/app/framework/angular/forms/date-time-editor.component.html b/frontend/app/framework/angular/forms/date-time-editor.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/date-time-editor.component.html rename to frontend/app/framework/angular/forms/date-time-editor.component.html diff --git a/src/Squidex/app/framework/angular/forms/date-time-editor.component.scss b/frontend/app/framework/angular/forms/date-time-editor.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/date-time-editor.component.scss rename to frontend/app/framework/angular/forms/date-time-editor.component.scss diff --git a/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts b/frontend/app/framework/angular/forms/date-time-editor.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/date-time-editor.component.ts rename to frontend/app/framework/angular/forms/date-time-editor.component.ts diff --git a/src/Squidex/app/framework/angular/forms/dropdown.component.html b/frontend/app/framework/angular/forms/dropdown.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/dropdown.component.html rename to frontend/app/framework/angular/forms/dropdown.component.html diff --git a/src/Squidex/app/framework/angular/forms/dropdown.component.scss b/frontend/app/framework/angular/forms/dropdown.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/dropdown.component.scss rename to frontend/app/framework/angular/forms/dropdown.component.scss diff --git a/src/Squidex/app/framework/angular/forms/dropdown.component.ts b/frontend/app/framework/angular/forms/dropdown.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/dropdown.component.ts rename to frontend/app/framework/angular/forms/dropdown.component.ts diff --git a/src/Squidex/app/framework/angular/forms/editable-title.component.html b/frontend/app/framework/angular/forms/editable-title.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/editable-title.component.html rename to frontend/app/framework/angular/forms/editable-title.component.html diff --git a/src/Squidex/app/framework/angular/forms/editable-title.component.scss b/frontend/app/framework/angular/forms/editable-title.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/editable-title.component.scss rename to frontend/app/framework/angular/forms/editable-title.component.scss diff --git a/src/Squidex/app/framework/angular/forms/editable-title.component.ts b/frontend/app/framework/angular/forms/editable-title.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/editable-title.component.ts rename to frontend/app/framework/angular/forms/editable-title.component.ts diff --git a/src/Squidex/app/framework/angular/forms/error-formatting.spec.ts b/frontend/app/framework/angular/forms/error-formatting.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/error-formatting.spec.ts rename to frontend/app/framework/angular/forms/error-formatting.spec.ts diff --git a/src/Squidex/app/framework/angular/forms/error-formatting.ts b/frontend/app/framework/angular/forms/error-formatting.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/error-formatting.ts rename to frontend/app/framework/angular/forms/error-formatting.ts diff --git a/src/Squidex/app/framework/angular/forms/file-drop.directive.ts b/frontend/app/framework/angular/forms/file-drop.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/file-drop.directive.ts rename to frontend/app/framework/angular/forms/file-drop.directive.ts diff --git a/src/Squidex/app/framework/angular/forms/focus-on-init.directive.spec.ts b/frontend/app/framework/angular/forms/focus-on-init.directive.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/focus-on-init.directive.spec.ts rename to frontend/app/framework/angular/forms/focus-on-init.directive.spec.ts diff --git a/src/Squidex/app/framework/angular/forms/focus-on-init.directive.ts b/frontend/app/framework/angular/forms/focus-on-init.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/focus-on-init.directive.ts rename to frontend/app/framework/angular/forms/focus-on-init.directive.ts diff --git a/src/Squidex/app/framework/angular/forms/form-alert.component.ts b/frontend/app/framework/angular/forms/form-alert.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/form-alert.component.ts rename to frontend/app/framework/angular/forms/form-alert.component.ts diff --git a/src/Squidex/app/framework/angular/forms/form-error.component.ts b/frontend/app/framework/angular/forms/form-error.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/form-error.component.ts rename to frontend/app/framework/angular/forms/form-error.component.ts diff --git a/src/Squidex/app/framework/angular/forms/form-hint.component.ts b/frontend/app/framework/angular/forms/form-hint.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/form-hint.component.ts rename to frontend/app/framework/angular/forms/form-hint.component.ts diff --git a/src/Squidex/app/framework/angular/forms/forms-helper.ts b/frontend/app/framework/angular/forms/forms-helper.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/forms-helper.ts rename to frontend/app/framework/angular/forms/forms-helper.ts diff --git a/src/Squidex/app/framework/angular/forms/iframe-editor.component.html b/frontend/app/framework/angular/forms/iframe-editor.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/iframe-editor.component.html rename to frontend/app/framework/angular/forms/iframe-editor.component.html diff --git a/src/Squidex/app/framework/angular/forms/iframe-editor.component.scss b/frontend/app/framework/angular/forms/iframe-editor.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/iframe-editor.component.scss rename to frontend/app/framework/angular/forms/iframe-editor.component.scss diff --git a/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts b/frontend/app/framework/angular/forms/iframe-editor.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/iframe-editor.component.ts rename to frontend/app/framework/angular/forms/iframe-editor.component.ts diff --git a/src/Squidex/app/framework/angular/forms/indeterminate-value.directive.ts b/frontend/app/framework/angular/forms/indeterminate-value.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/indeterminate-value.directive.ts rename to frontend/app/framework/angular/forms/indeterminate-value.directive.ts diff --git a/src/Squidex/app/framework/angular/forms/json-editor.component.html b/frontend/app/framework/angular/forms/json-editor.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/json-editor.component.html rename to frontend/app/framework/angular/forms/json-editor.component.html diff --git a/src/Squidex/app/framework/angular/forms/json-editor.component.scss b/frontend/app/framework/angular/forms/json-editor.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/json-editor.component.scss rename to frontend/app/framework/angular/forms/json-editor.component.scss diff --git a/src/Squidex/app/framework/angular/forms/json-editor.component.ts b/frontend/app/framework/angular/forms/json-editor.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/json-editor.component.ts rename to frontend/app/framework/angular/forms/json-editor.component.ts diff --git a/src/Squidex/app/framework/angular/forms/progress-bar.component.ts b/frontend/app/framework/angular/forms/progress-bar.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/progress-bar.component.ts rename to frontend/app/framework/angular/forms/progress-bar.component.ts diff --git a/src/Squidex/app/framework/angular/forms/stars.component.html b/frontend/app/framework/angular/forms/stars.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/stars.component.html rename to frontend/app/framework/angular/forms/stars.component.html diff --git a/src/Squidex/app/framework/angular/forms/stars.component.scss b/frontend/app/framework/angular/forms/stars.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/stars.component.scss rename to frontend/app/framework/angular/forms/stars.component.scss diff --git a/src/Squidex/app/framework/angular/forms/stars.component.ts b/frontend/app/framework/angular/forms/stars.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/stars.component.ts rename to frontend/app/framework/angular/forms/stars.component.ts diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.html b/frontend/app/framework/angular/forms/tag-editor.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/tag-editor.component.html rename to frontend/app/framework/angular/forms/tag-editor.component.html diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.scss b/frontend/app/framework/angular/forms/tag-editor.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/tag-editor.component.scss rename to frontend/app/framework/angular/forms/tag-editor.component.scss diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts b/frontend/app/framework/angular/forms/tag-editor.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/tag-editor.component.ts rename to frontend/app/framework/angular/forms/tag-editor.component.ts diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.html b/frontend/app/framework/angular/forms/toggle.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/toggle.component.html rename to frontend/app/framework/angular/forms/toggle.component.html diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.scss b/frontend/app/framework/angular/forms/toggle.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/toggle.component.scss rename to frontend/app/framework/angular/forms/toggle.component.scss diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.ts b/frontend/app/framework/angular/forms/toggle.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/toggle.component.ts rename to frontend/app/framework/angular/forms/toggle.component.ts diff --git a/src/Squidex/app/framework/angular/forms/transform-input.directive.ts b/frontend/app/framework/angular/forms/transform-input.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/transform-input.directive.ts rename to frontend/app/framework/angular/forms/transform-input.directive.ts diff --git a/src/Squidex/app/framework/angular/forms/validators.spec.ts b/frontend/app/framework/angular/forms/validators.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/validators.spec.ts rename to frontend/app/framework/angular/forms/validators.spec.ts diff --git a/src/Squidex/app/framework/angular/forms/validators.ts b/frontend/app/framework/angular/forms/validators.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/validators.ts rename to frontend/app/framework/angular/forms/validators.ts diff --git a/src/Squidex/app/framework/angular/highlight.pipe.ts b/frontend/app/framework/angular/highlight.pipe.ts similarity index 100% rename from src/Squidex/app/framework/angular/highlight.pipe.ts rename to frontend/app/framework/angular/highlight.pipe.ts diff --git a/src/Squidex/app/framework/angular/hover-background.directive.ts b/frontend/app/framework/angular/hover-background.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/hover-background.directive.ts rename to frontend/app/framework/angular/hover-background.directive.ts diff --git a/src/Squidex/app/framework/angular/http/caching.interceptor.ts b/frontend/app/framework/angular/http/caching.interceptor.ts similarity index 100% rename from src/Squidex/app/framework/angular/http/caching.interceptor.ts rename to frontend/app/framework/angular/http/caching.interceptor.ts diff --git a/src/Squidex/app/framework/angular/http/http-extensions.ts b/frontend/app/framework/angular/http/http-extensions.ts similarity index 100% rename from src/Squidex/app/framework/angular/http/http-extensions.ts rename to frontend/app/framework/angular/http/http-extensions.ts diff --git a/src/Squidex/app/framework/angular/http/loading.interceptor.ts b/frontend/app/framework/angular/http/loading.interceptor.ts similarity index 100% rename from src/Squidex/app/framework/angular/http/loading.interceptor.ts rename to frontend/app/framework/angular/http/loading.interceptor.ts diff --git a/src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts b/frontend/app/framework/angular/ignore-scrollbar.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts rename to frontend/app/framework/angular/ignore-scrollbar.directive.ts diff --git a/src/Squidex/app/framework/angular/image-source.directive.ts b/frontend/app/framework/angular/image-source.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/image-source.directive.ts rename to frontend/app/framework/angular/image-source.directive.ts diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.html b/frontend/app/framework/angular/modals/dialog-renderer.component.html similarity index 100% rename from src/Squidex/app/framework/angular/modals/dialog-renderer.component.html rename to frontend/app/framework/angular/modals/dialog-renderer.component.html diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.scss b/frontend/app/framework/angular/modals/dialog-renderer.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/modals/dialog-renderer.component.scss rename to frontend/app/framework/angular/modals/dialog-renderer.component.scss diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts b/frontend/app/framework/angular/modals/dialog-renderer.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts rename to frontend/app/framework/angular/modals/dialog-renderer.component.ts diff --git a/src/Squidex/app/framework/angular/modals/modal-dialog.component.html b/frontend/app/framework/angular/modals/modal-dialog.component.html similarity index 100% rename from src/Squidex/app/framework/angular/modals/modal-dialog.component.html rename to frontend/app/framework/angular/modals/modal-dialog.component.html diff --git a/src/Squidex/app/framework/angular/modals/modal-dialog.component.scss b/frontend/app/framework/angular/modals/modal-dialog.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/modals/modal-dialog.component.scss rename to frontend/app/framework/angular/modals/modal-dialog.component.scss diff --git a/src/Squidex/app/framework/angular/modals/modal-dialog.component.ts b/frontend/app/framework/angular/modals/modal-dialog.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/modals/modal-dialog.component.ts rename to frontend/app/framework/angular/modals/modal-dialog.component.ts diff --git a/src/Squidex/app/framework/angular/modals/modal-placement.directive.ts b/frontend/app/framework/angular/modals/modal-placement.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/modals/modal-placement.directive.ts rename to frontend/app/framework/angular/modals/modal-placement.directive.ts diff --git a/src/Squidex/app/framework/angular/modals/modal.directive.ts b/frontend/app/framework/angular/modals/modal.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/modals/modal.directive.ts rename to frontend/app/framework/angular/modals/modal.directive.ts diff --git a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.html b/frontend/app/framework/angular/modals/onboarding-tooltip.component.html similarity index 100% rename from src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.html rename to frontend/app/framework/angular/modals/onboarding-tooltip.component.html diff --git a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.scss b/frontend/app/framework/angular/modals/onboarding-tooltip.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.scss rename to frontend/app/framework/angular/modals/onboarding-tooltip.component.scss diff --git a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts b/frontend/app/framework/angular/modals/onboarding-tooltip.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts rename to frontend/app/framework/angular/modals/onboarding-tooltip.component.ts diff --git a/src/Squidex/app/framework/angular/modals/root-view.component.ts b/frontend/app/framework/angular/modals/root-view.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/modals/root-view.component.ts rename to frontend/app/framework/angular/modals/root-view.component.ts diff --git a/src/Squidex/app/framework/angular/modals/tooltip.directive.ts b/frontend/app/framework/angular/modals/tooltip.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/modals/tooltip.directive.ts rename to frontend/app/framework/angular/modals/tooltip.directive.ts diff --git a/src/Squidex/app/framework/angular/pager.component.html b/frontend/app/framework/angular/pager.component.html similarity index 100% rename from src/Squidex/app/framework/angular/pager.component.html rename to frontend/app/framework/angular/pager.component.html diff --git a/src/Squidex/app/framework/angular/pager.component.scss b/frontend/app/framework/angular/pager.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/pager.component.scss rename to frontend/app/framework/angular/pager.component.scss diff --git a/src/Squidex/app/framework/angular/pager.component.ts b/frontend/app/framework/angular/pager.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/pager.component.ts rename to frontend/app/framework/angular/pager.component.ts diff --git a/src/Squidex/app/framework/angular/panel-container.directive.ts b/frontend/app/framework/angular/panel-container.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/panel-container.directive.ts rename to frontend/app/framework/angular/panel-container.directive.ts diff --git a/src/Squidex/app/framework/angular/panel.component.html b/frontend/app/framework/angular/panel.component.html similarity index 100% rename from src/Squidex/app/framework/angular/panel.component.html rename to frontend/app/framework/angular/panel.component.html diff --git a/src/Squidex/app/framework/angular/panel.component.scss b/frontend/app/framework/angular/panel.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/panel.component.scss rename to frontend/app/framework/angular/panel.component.scss diff --git a/src/Squidex/app/framework/angular/panel.component.ts b/frontend/app/framework/angular/panel.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/panel.component.ts rename to frontend/app/framework/angular/panel.component.ts diff --git a/src/Squidex/app/framework/angular/pipes/colors.pipes.spec.ts b/frontend/app/framework/angular/pipes/colors.pipes.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/colors.pipes.spec.ts rename to frontend/app/framework/angular/pipes/colors.pipes.spec.ts diff --git a/src/Squidex/app/framework/angular/pipes/colors.pipes.ts b/frontend/app/framework/angular/pipes/colors.pipes.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/colors.pipes.ts rename to frontend/app/framework/angular/pipes/colors.pipes.ts diff --git a/src/Squidex/app/framework/angular/pipes/date-time.pipes.spec.ts b/frontend/app/framework/angular/pipes/date-time.pipes.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/date-time.pipes.spec.ts rename to frontend/app/framework/angular/pipes/date-time.pipes.spec.ts diff --git a/src/Squidex/app/framework/angular/pipes/date-time.pipes.ts b/frontend/app/framework/angular/pipes/date-time.pipes.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/date-time.pipes.ts rename to frontend/app/framework/angular/pipes/date-time.pipes.ts diff --git a/src/Squidex/app/framework/angular/pipes/keys.pipe.spec.ts b/frontend/app/framework/angular/pipes/keys.pipe.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/keys.pipe.spec.ts rename to frontend/app/framework/angular/pipes/keys.pipe.spec.ts diff --git a/src/Squidex/app/framework/angular/pipes/keys.pipe.ts b/frontend/app/framework/angular/pipes/keys.pipe.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/keys.pipe.ts rename to frontend/app/framework/angular/pipes/keys.pipe.ts diff --git a/src/Squidex/app/framework/angular/pipes/markdown.pipe.spec.ts b/frontend/app/framework/angular/pipes/markdown.pipe.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/markdown.pipe.spec.ts rename to frontend/app/framework/angular/pipes/markdown.pipe.spec.ts diff --git a/src/Squidex/app/framework/angular/pipes/markdown.pipe.ts b/frontend/app/framework/angular/pipes/markdown.pipe.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/markdown.pipe.ts rename to frontend/app/framework/angular/pipes/markdown.pipe.ts diff --git a/src/Squidex/app/framework/angular/pipes/money.pipe.spec.ts b/frontend/app/framework/angular/pipes/money.pipe.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/money.pipe.spec.ts rename to frontend/app/framework/angular/pipes/money.pipe.spec.ts diff --git a/src/Squidex/app/framework/angular/pipes/money.pipe.ts b/frontend/app/framework/angular/pipes/money.pipe.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/money.pipe.ts rename to frontend/app/framework/angular/pipes/money.pipe.ts diff --git a/src/Squidex/app/framework/angular/pipes/name.pipe.spec.ts b/frontend/app/framework/angular/pipes/name.pipe.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/name.pipe.spec.ts rename to frontend/app/framework/angular/pipes/name.pipe.spec.ts diff --git a/src/Squidex/app/framework/angular/pipes/name.pipe.ts b/frontend/app/framework/angular/pipes/name.pipe.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/name.pipe.ts rename to frontend/app/framework/angular/pipes/name.pipe.ts diff --git a/src/Squidex/app/framework/angular/pipes/numbers.pipes.spec.ts b/frontend/app/framework/angular/pipes/numbers.pipes.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/numbers.pipes.spec.ts rename to frontend/app/framework/angular/pipes/numbers.pipes.spec.ts diff --git a/src/Squidex/app/framework/angular/pipes/numbers.pipes.ts b/frontend/app/framework/angular/pipes/numbers.pipes.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/numbers.pipes.ts rename to frontend/app/framework/angular/pipes/numbers.pipes.ts diff --git a/src/Squidex/app/framework/angular/popup-link.directive.ts b/frontend/app/framework/angular/popup-link.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/popup-link.directive.ts rename to frontend/app/framework/angular/popup-link.directive.ts diff --git a/src/Squidex/app/framework/angular/routers/can-deactivate.guard.spec.ts b/frontend/app/framework/angular/routers/can-deactivate.guard.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/routers/can-deactivate.guard.spec.ts rename to frontend/app/framework/angular/routers/can-deactivate.guard.spec.ts diff --git a/src/Squidex/app/framework/angular/routers/can-deactivate.guard.ts b/frontend/app/framework/angular/routers/can-deactivate.guard.ts similarity index 100% rename from src/Squidex/app/framework/angular/routers/can-deactivate.guard.ts rename to frontend/app/framework/angular/routers/can-deactivate.guard.ts diff --git a/src/Squidex/app/framework/angular/routers/parent-link.directive.ts b/frontend/app/framework/angular/routers/parent-link.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/routers/parent-link.directive.ts rename to frontend/app/framework/angular/routers/parent-link.directive.ts diff --git a/src/Squidex/app/framework/angular/routers/router-utils.spec.ts b/frontend/app/framework/angular/routers/router-utils.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/routers/router-utils.spec.ts rename to frontend/app/framework/angular/routers/router-utils.spec.ts diff --git a/src/Squidex/app/framework/angular/routers/router-utils.ts b/frontend/app/framework/angular/routers/router-utils.ts similarity index 100% rename from src/Squidex/app/framework/angular/routers/router-utils.ts rename to frontend/app/framework/angular/routers/router-utils.ts diff --git a/src/Squidex/app/framework/angular/safe-html.pipe.ts b/frontend/app/framework/angular/safe-html.pipe.ts similarity index 100% rename from src/Squidex/app/framework/angular/safe-html.pipe.ts rename to frontend/app/framework/angular/safe-html.pipe.ts diff --git a/src/Squidex/app/framework/angular/scroll-active.directive.ts b/frontend/app/framework/angular/scroll-active.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/scroll-active.directive.ts rename to frontend/app/framework/angular/scroll-active.directive.ts diff --git a/src/Squidex/app/framework/angular/shortcut.component.spec.ts b/frontend/app/framework/angular/shortcut.component.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/shortcut.component.spec.ts rename to frontend/app/framework/angular/shortcut.component.spec.ts diff --git a/src/Squidex/app/framework/angular/shortcut.component.ts b/frontend/app/framework/angular/shortcut.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/shortcut.component.ts rename to frontend/app/framework/angular/shortcut.component.ts diff --git a/src/Squidex/app/framework/angular/stateful.component.ts b/frontend/app/framework/angular/stateful.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/stateful.component.ts rename to frontend/app/framework/angular/stateful.component.ts diff --git a/src/Squidex/app/framework/angular/status-icon.component.html b/frontend/app/framework/angular/status-icon.component.html similarity index 100% rename from src/Squidex/app/framework/angular/status-icon.component.html rename to frontend/app/framework/angular/status-icon.component.html diff --git a/src/Squidex/app/framework/angular/status-icon.component.scss b/frontend/app/framework/angular/status-icon.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/status-icon.component.scss rename to frontend/app/framework/angular/status-icon.component.scss diff --git a/src/Squidex/app/framework/angular/status-icon.component.ts b/frontend/app/framework/angular/status-icon.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/status-icon.component.ts rename to frontend/app/framework/angular/status-icon.component.ts diff --git a/src/Squidex/app/framework/angular/stop-click.directive.ts b/frontend/app/framework/angular/stop-click.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/stop-click.directive.ts rename to frontend/app/framework/angular/stop-click.directive.ts diff --git a/src/Squidex/app/framework/angular/sync-scrolling.directive.ts b/frontend/app/framework/angular/sync-scrolling.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/sync-scrolling.directive.ts rename to frontend/app/framework/angular/sync-scrolling.directive.ts diff --git a/src/Squidex/app/framework/angular/template-wrapper.directive.ts b/frontend/app/framework/angular/template-wrapper.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/template-wrapper.directive.ts rename to frontend/app/framework/angular/template-wrapper.directive.ts diff --git a/src/Squidex/app/framework/angular/title.component.ts b/frontend/app/framework/angular/title.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/title.component.ts rename to frontend/app/framework/angular/title.component.ts diff --git a/src/Squidex/app/framework/configurations.ts b/frontend/app/framework/configurations.ts similarity index 100% rename from src/Squidex/app/framework/configurations.ts rename to frontend/app/framework/configurations.ts diff --git a/src/Squidex/app/framework/declarations.ts b/frontend/app/framework/declarations.ts similarity index 100% rename from src/Squidex/app/framework/declarations.ts rename to frontend/app/framework/declarations.ts diff --git a/src/Squidex/app/framework/index.ts b/frontend/app/framework/index.ts similarity index 100% rename from src/Squidex/app/framework/index.ts rename to frontend/app/framework/index.ts diff --git a/src/Squidex/app/framework/internal.ts b/frontend/app/framework/internal.ts similarity index 100% rename from src/Squidex/app/framework/internal.ts rename to frontend/app/framework/internal.ts diff --git a/src/Squidex/app/framework/module.ts b/frontend/app/framework/module.ts similarity index 100% rename from src/Squidex/app/framework/module.ts rename to frontend/app/framework/module.ts diff --git a/src/Squidex/app/framework/services/analytics.service.ts b/frontend/app/framework/services/analytics.service.ts similarity index 100% rename from src/Squidex/app/framework/services/analytics.service.ts rename to frontend/app/framework/services/analytics.service.ts diff --git a/src/Squidex/app/framework/services/clipboard.service.spec.ts b/frontend/app/framework/services/clipboard.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/clipboard.service.spec.ts rename to frontend/app/framework/services/clipboard.service.spec.ts diff --git a/src/Squidex/app/framework/services/clipboard.service.ts b/frontend/app/framework/services/clipboard.service.ts similarity index 100% rename from src/Squidex/app/framework/services/clipboard.service.ts rename to frontend/app/framework/services/clipboard.service.ts diff --git a/src/Squidex/app/framework/services/dialog.service.spec.ts b/frontend/app/framework/services/dialog.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/dialog.service.spec.ts rename to frontend/app/framework/services/dialog.service.spec.ts diff --git a/src/Squidex/app/framework/services/dialog.service.ts b/frontend/app/framework/services/dialog.service.ts similarity index 100% rename from src/Squidex/app/framework/services/dialog.service.ts rename to frontend/app/framework/services/dialog.service.ts diff --git a/src/Squidex/app/framework/services/loading.service.spec.ts b/frontend/app/framework/services/loading.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/loading.service.spec.ts rename to frontend/app/framework/services/loading.service.spec.ts diff --git a/src/Squidex/app/framework/services/loading.service.ts b/frontend/app/framework/services/loading.service.ts similarity index 100% rename from src/Squidex/app/framework/services/loading.service.ts rename to frontend/app/framework/services/loading.service.ts diff --git a/src/Squidex/app/framework/services/local-store.service.spec.ts b/frontend/app/framework/services/local-store.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/local-store.service.spec.ts rename to frontend/app/framework/services/local-store.service.spec.ts diff --git a/src/Squidex/app/framework/services/local-store.service.ts b/frontend/app/framework/services/local-store.service.ts similarity index 100% rename from src/Squidex/app/framework/services/local-store.service.ts rename to frontend/app/framework/services/local-store.service.ts diff --git a/src/Squidex/app/framework/services/message-bus.service.spec.ts b/frontend/app/framework/services/message-bus.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/message-bus.service.spec.ts rename to frontend/app/framework/services/message-bus.service.spec.ts diff --git a/src/Squidex/app/framework/services/message-bus.service.ts b/frontend/app/framework/services/message-bus.service.ts similarity index 100% rename from src/Squidex/app/framework/services/message-bus.service.ts rename to frontend/app/framework/services/message-bus.service.ts diff --git a/src/Squidex/app/framework/services/onboarding.service.spec.ts b/frontend/app/framework/services/onboarding.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/onboarding.service.spec.ts rename to frontend/app/framework/services/onboarding.service.spec.ts diff --git a/src/Squidex/app/framework/services/onboarding.service.ts b/frontend/app/framework/services/onboarding.service.ts similarity index 100% rename from src/Squidex/app/framework/services/onboarding.service.ts rename to frontend/app/framework/services/onboarding.service.ts diff --git a/src/Squidex/app/framework/services/resource-loader.service.ts b/frontend/app/framework/services/resource-loader.service.ts similarity index 100% rename from src/Squidex/app/framework/services/resource-loader.service.ts rename to frontend/app/framework/services/resource-loader.service.ts diff --git a/src/Squidex/app/framework/services/shortcut.service.spec.ts b/frontend/app/framework/services/shortcut.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/shortcut.service.spec.ts rename to frontend/app/framework/services/shortcut.service.spec.ts diff --git a/src/Squidex/app/framework/services/shortcut.service.ts b/frontend/app/framework/services/shortcut.service.ts similarity index 100% rename from src/Squidex/app/framework/services/shortcut.service.ts rename to frontend/app/framework/services/shortcut.service.ts diff --git a/src/Squidex/app/framework/services/title.service.spec.ts b/frontend/app/framework/services/title.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/title.service.spec.ts rename to frontend/app/framework/services/title.service.spec.ts diff --git a/src/Squidex/app/framework/services/title.service.ts b/frontend/app/framework/services/title.service.ts similarity index 100% rename from src/Squidex/app/framework/services/title.service.ts rename to frontend/app/framework/services/title.service.ts diff --git a/src/Squidex/app/framework/state.ts b/frontend/app/framework/state.ts similarity index 100% rename from src/Squidex/app/framework/state.ts rename to frontend/app/framework/state.ts diff --git a/src/Squidex/app/framework/utils/array-extensions.spec.ts b/frontend/app/framework/utils/array-extensions.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/array-extensions.spec.ts rename to frontend/app/framework/utils/array-extensions.spec.ts diff --git a/src/Squidex/app/framework/utils/array-extensions.ts b/frontend/app/framework/utils/array-extensions.ts similarity index 100% rename from src/Squidex/app/framework/utils/array-extensions.ts rename to frontend/app/framework/utils/array-extensions.ts diff --git a/src/Squidex/app/framework/utils/array-helper.ts b/frontend/app/framework/utils/array-helper.ts similarity index 100% rename from src/Squidex/app/framework/utils/array-helper.ts rename to frontend/app/framework/utils/array-helper.ts diff --git a/src/Squidex/app/framework/utils/date-helper.spec.ts b/frontend/app/framework/utils/date-helper.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/date-helper.spec.ts rename to frontend/app/framework/utils/date-helper.spec.ts diff --git a/src/Squidex/app/framework/utils/date-helper.ts b/frontend/app/framework/utils/date-helper.ts similarity index 100% rename from src/Squidex/app/framework/utils/date-helper.ts rename to frontend/app/framework/utils/date-helper.ts diff --git a/src/Squidex/app/framework/utils/date-time.spec.ts b/frontend/app/framework/utils/date-time.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/date-time.spec.ts rename to frontend/app/framework/utils/date-time.spec.ts diff --git a/src/Squidex/app/framework/utils/date-time.ts b/frontend/app/framework/utils/date-time.ts similarity index 100% rename from src/Squidex/app/framework/utils/date-time.ts rename to frontend/app/framework/utils/date-time.ts diff --git a/src/Squidex/app/framework/utils/duration.spec.ts b/frontend/app/framework/utils/duration.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/duration.spec.ts rename to frontend/app/framework/utils/duration.spec.ts diff --git a/src/Squidex/app/framework/utils/duration.ts b/frontend/app/framework/utils/duration.ts similarity index 100% rename from src/Squidex/app/framework/utils/duration.ts rename to frontend/app/framework/utils/duration.ts diff --git a/src/Squidex/app/framework/utils/error.spec.ts b/frontend/app/framework/utils/error.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/error.spec.ts rename to frontend/app/framework/utils/error.spec.ts diff --git a/src/Squidex/app/framework/utils/error.ts b/frontend/app/framework/utils/error.ts similarity index 100% rename from src/Squidex/app/framework/utils/error.ts rename to frontend/app/framework/utils/error.ts diff --git a/src/Squidex/app/framework/utils/hateos.ts b/frontend/app/framework/utils/hateos.ts similarity index 100% rename from src/Squidex/app/framework/utils/hateos.ts rename to frontend/app/framework/utils/hateos.ts diff --git a/src/Squidex/app/framework/utils/interpolator.spec.ts b/frontend/app/framework/utils/interpolator.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/interpolator.spec.ts rename to frontend/app/framework/utils/interpolator.spec.ts diff --git a/src/Squidex/app/framework/utils/interpolator.ts b/frontend/app/framework/utils/interpolator.ts similarity index 100% rename from src/Squidex/app/framework/utils/interpolator.ts rename to frontend/app/framework/utils/interpolator.ts diff --git a/src/Squidex/app/framework/utils/keys.ts b/frontend/app/framework/utils/keys.ts similarity index 100% rename from src/Squidex/app/framework/utils/keys.ts rename to frontend/app/framework/utils/keys.ts diff --git a/src/Squidex/app/framework/utils/math-helper.spec.ts b/frontend/app/framework/utils/math-helper.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/math-helper.spec.ts rename to frontend/app/framework/utils/math-helper.spec.ts diff --git a/src/Squidex/app/framework/utils/math-helper.ts b/frontend/app/framework/utils/math-helper.ts similarity index 100% rename from src/Squidex/app/framework/utils/math-helper.ts rename to frontend/app/framework/utils/math-helper.ts diff --git a/src/Squidex/app/framework/utils/modal-positioner.spec.ts b/frontend/app/framework/utils/modal-positioner.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/modal-positioner.spec.ts rename to frontend/app/framework/utils/modal-positioner.spec.ts diff --git a/src/Squidex/app/framework/utils/modal-positioner.ts b/frontend/app/framework/utils/modal-positioner.ts similarity index 100% rename from src/Squidex/app/framework/utils/modal-positioner.ts rename to frontend/app/framework/utils/modal-positioner.ts diff --git a/src/Squidex/app/framework/utils/modal-view.spec.ts b/frontend/app/framework/utils/modal-view.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/modal-view.spec.ts rename to frontend/app/framework/utils/modal-view.spec.ts diff --git a/src/Squidex/app/framework/utils/modal-view.ts b/frontend/app/framework/utils/modal-view.ts similarity index 100% rename from src/Squidex/app/framework/utils/modal-view.ts rename to frontend/app/framework/utils/modal-view.ts diff --git a/src/Squidex/app/framework/utils/pager.spec.ts b/frontend/app/framework/utils/pager.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/pager.spec.ts rename to frontend/app/framework/utils/pager.spec.ts diff --git a/src/Squidex/app/framework/utils/pager.ts b/frontend/app/framework/utils/pager.ts similarity index 100% rename from src/Squidex/app/framework/utils/pager.ts rename to frontend/app/framework/utils/pager.ts diff --git a/src/Squidex/app/framework/utils/picasso.ts b/frontend/app/framework/utils/picasso.ts similarity index 100% rename from src/Squidex/app/framework/utils/picasso.ts rename to frontend/app/framework/utils/picasso.ts diff --git a/src/Squidex/app/framework/utils/rxjs-extensions.ts b/frontend/app/framework/utils/rxjs-extensions.ts similarity index 100% rename from src/Squidex/app/framework/utils/rxjs-extensions.ts rename to frontend/app/framework/utils/rxjs-extensions.ts diff --git a/src/Squidex/app/framework/utils/string-helper.spec.ts b/frontend/app/framework/utils/string-helper.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/string-helper.spec.ts rename to frontend/app/framework/utils/string-helper.spec.ts diff --git a/src/Squidex/app/framework/utils/string-helper.ts b/frontend/app/framework/utils/string-helper.ts similarity index 100% rename from src/Squidex/app/framework/utils/string-helper.ts rename to frontend/app/framework/utils/string-helper.ts diff --git a/src/Squidex/app/framework/utils/types.spec.ts b/frontend/app/framework/utils/types.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/types.spec.ts rename to frontend/app/framework/utils/types.spec.ts diff --git a/src/Squidex/app/framework/utils/types.ts b/frontend/app/framework/utils/types.ts similarity index 100% rename from src/Squidex/app/framework/utils/types.ts rename to frontend/app/framework/utils/types.ts diff --git a/src/Squidex/app/framework/utils/version.spec.ts b/frontend/app/framework/utils/version.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/version.spec.ts rename to frontend/app/framework/utils/version.spec.ts diff --git a/src/Squidex/app/framework/utils/version.ts b/frontend/app/framework/utils/version.ts similarity index 100% rename from src/Squidex/app/framework/utils/version.ts rename to frontend/app/framework/utils/version.ts diff --git a/src/Squidex/wwwroot/index.html b/frontend/app/index.html similarity index 100% rename from src/Squidex/wwwroot/index.html rename to frontend/app/index.html diff --git a/src/Squidex/app/shared/components/app-form.component.html b/frontend/app/shared/components/app-form.component.html similarity index 100% rename from src/Squidex/app/shared/components/app-form.component.html rename to frontend/app/shared/components/app-form.component.html diff --git a/src/Squidex/app/shared/components/app-form.component.scss b/frontend/app/shared/components/app-form.component.scss similarity index 100% rename from src/Squidex/app/shared/components/app-form.component.scss rename to frontend/app/shared/components/app-form.component.scss diff --git a/src/Squidex/app/shared/components/app-form.component.ts b/frontend/app/shared/components/app-form.component.ts similarity index 100% rename from src/Squidex/app/shared/components/app-form.component.ts rename to frontend/app/shared/components/app-form.component.ts diff --git a/src/Squidex/app/shared/components/asset-dialog.component.html b/frontend/app/shared/components/asset-dialog.component.html similarity index 100% rename from src/Squidex/app/shared/components/asset-dialog.component.html rename to frontend/app/shared/components/asset-dialog.component.html diff --git a/src/Squidex/app/shared/components/asset-dialog.component.scss b/frontend/app/shared/components/asset-dialog.component.scss similarity index 100% rename from src/Squidex/app/shared/components/asset-dialog.component.scss rename to frontend/app/shared/components/asset-dialog.component.scss diff --git a/src/Squidex/app/shared/components/asset-dialog.component.ts b/frontend/app/shared/components/asset-dialog.component.ts similarity index 100% rename from src/Squidex/app/shared/components/asset-dialog.component.ts rename to frontend/app/shared/components/asset-dialog.component.ts diff --git a/src/Squidex/app/shared/components/asset-uploader.component.html b/frontend/app/shared/components/asset-uploader.component.html similarity index 100% rename from src/Squidex/app/shared/components/asset-uploader.component.html rename to frontend/app/shared/components/asset-uploader.component.html diff --git a/src/Squidex/app/shared/components/asset-uploader.component.scss b/frontend/app/shared/components/asset-uploader.component.scss similarity index 100% rename from src/Squidex/app/shared/components/asset-uploader.component.scss rename to frontend/app/shared/components/asset-uploader.component.scss diff --git a/src/Squidex/app/shared/components/asset-uploader.component.ts b/frontend/app/shared/components/asset-uploader.component.ts similarity index 100% rename from src/Squidex/app/shared/components/asset-uploader.component.ts rename to frontend/app/shared/components/asset-uploader.component.ts diff --git a/src/Squidex/app/shared/components/asset.component.html b/frontend/app/shared/components/asset.component.html similarity index 100% rename from src/Squidex/app/shared/components/asset.component.html rename to frontend/app/shared/components/asset.component.html diff --git a/src/Squidex/app/shared/components/asset.component.scss b/frontend/app/shared/components/asset.component.scss similarity index 100% rename from src/Squidex/app/shared/components/asset.component.scss rename to frontend/app/shared/components/asset.component.scss diff --git a/src/Squidex/app/shared/components/asset.component.ts b/frontend/app/shared/components/asset.component.ts similarity index 100% rename from src/Squidex/app/shared/components/asset.component.ts rename to frontend/app/shared/components/asset.component.ts diff --git a/src/Squidex/app/shared/components/assets-list.component.html b/frontend/app/shared/components/assets-list.component.html similarity index 100% rename from src/Squidex/app/shared/components/assets-list.component.html rename to frontend/app/shared/components/assets-list.component.html diff --git a/src/Squidex/app/shared/components/assets-list.component.scss b/frontend/app/shared/components/assets-list.component.scss similarity index 100% rename from src/Squidex/app/shared/components/assets-list.component.scss rename to frontend/app/shared/components/assets-list.component.scss diff --git a/src/Squidex/app/shared/components/assets-list.component.ts b/frontend/app/shared/components/assets-list.component.ts similarity index 100% rename from src/Squidex/app/shared/components/assets-list.component.ts rename to frontend/app/shared/components/assets-list.component.ts diff --git a/src/Squidex/app/shared/components/assets-selector.component.html b/frontend/app/shared/components/assets-selector.component.html similarity index 100% rename from src/Squidex/app/shared/components/assets-selector.component.html rename to frontend/app/shared/components/assets-selector.component.html diff --git a/src/Squidex/app/shared/components/assets-selector.component.scss b/frontend/app/shared/components/assets-selector.component.scss similarity index 100% rename from src/Squidex/app/shared/components/assets-selector.component.scss rename to frontend/app/shared/components/assets-selector.component.scss diff --git a/src/Squidex/app/shared/components/assets-selector.component.ts b/frontend/app/shared/components/assets-selector.component.ts similarity index 100% rename from src/Squidex/app/shared/components/assets-selector.component.ts rename to frontend/app/shared/components/assets-selector.component.ts diff --git a/src/Squidex/app/shared/components/comment.component.html b/frontend/app/shared/components/comment.component.html similarity index 100% rename from src/Squidex/app/shared/components/comment.component.html rename to frontend/app/shared/components/comment.component.html diff --git a/src/Squidex/app/shared/components/comment.component.scss b/frontend/app/shared/components/comment.component.scss similarity index 100% rename from src/Squidex/app/shared/components/comment.component.scss rename to frontend/app/shared/components/comment.component.scss diff --git a/src/Squidex/app/shared/components/comment.component.ts b/frontend/app/shared/components/comment.component.ts similarity index 100% rename from src/Squidex/app/shared/components/comment.component.ts rename to frontend/app/shared/components/comment.component.ts diff --git a/src/Squidex/app/shared/components/comments.component.html b/frontend/app/shared/components/comments.component.html similarity index 100% rename from src/Squidex/app/shared/components/comments.component.html rename to frontend/app/shared/components/comments.component.html diff --git a/src/Squidex/app/shared/components/comments.component.scss b/frontend/app/shared/components/comments.component.scss similarity index 100% rename from src/Squidex/app/shared/components/comments.component.scss rename to frontend/app/shared/components/comments.component.scss diff --git a/src/Squidex/app/shared/components/comments.component.ts b/frontend/app/shared/components/comments.component.ts similarity index 100% rename from src/Squidex/app/shared/components/comments.component.ts rename to frontend/app/shared/components/comments.component.ts diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.html b/frontend/app/shared/components/geolocation-editor.component.html similarity index 100% rename from src/Squidex/app/shared/components/geolocation-editor.component.html rename to frontend/app/shared/components/geolocation-editor.component.html diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.scss b/frontend/app/shared/components/geolocation-editor.component.scss similarity index 100% rename from src/Squidex/app/shared/components/geolocation-editor.component.scss rename to frontend/app/shared/components/geolocation-editor.component.scss diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.ts b/frontend/app/shared/components/geolocation-editor.component.ts similarity index 100% rename from src/Squidex/app/shared/components/geolocation-editor.component.ts rename to frontend/app/shared/components/geolocation-editor.component.ts diff --git a/src/Squidex/app/shared/components/help-markdown.pipe.spec.ts b/frontend/app/shared/components/help-markdown.pipe.spec.ts similarity index 100% rename from src/Squidex/app/shared/components/help-markdown.pipe.spec.ts rename to frontend/app/shared/components/help-markdown.pipe.spec.ts diff --git a/src/Squidex/app/shared/components/help-markdown.pipe.ts b/frontend/app/shared/components/help-markdown.pipe.ts similarity index 100% rename from src/Squidex/app/shared/components/help-markdown.pipe.ts rename to frontend/app/shared/components/help-markdown.pipe.ts diff --git a/src/Squidex/app/shared/components/help.component.html b/frontend/app/shared/components/help.component.html similarity index 100% rename from src/Squidex/app/shared/components/help.component.html rename to frontend/app/shared/components/help.component.html diff --git a/src/Squidex/app/shared/components/help.component.scss b/frontend/app/shared/components/help.component.scss similarity index 100% rename from src/Squidex/app/shared/components/help.component.scss rename to frontend/app/shared/components/help.component.scss diff --git a/src/Squidex/app/shared/components/help.component.ts b/frontend/app/shared/components/help.component.ts similarity index 100% rename from src/Squidex/app/shared/components/help.component.ts rename to frontend/app/shared/components/help.component.ts diff --git a/src/Squidex/app/shared/components/history-list.component.html b/frontend/app/shared/components/history-list.component.html similarity index 100% rename from src/Squidex/app/shared/components/history-list.component.html rename to frontend/app/shared/components/history-list.component.html diff --git a/src/Squidex/app/shared/components/history-list.component.scss b/frontend/app/shared/components/history-list.component.scss similarity index 100% rename from src/Squidex/app/shared/components/history-list.component.scss rename to frontend/app/shared/components/history-list.component.scss diff --git a/src/Squidex/app/shared/components/history-list.component.ts b/frontend/app/shared/components/history-list.component.ts similarity index 100% rename from src/Squidex/app/shared/components/history-list.component.ts rename to frontend/app/shared/components/history-list.component.ts diff --git a/src/Squidex/app/shared/components/history.component.html b/frontend/app/shared/components/history.component.html similarity index 100% rename from src/Squidex/app/shared/components/history.component.html rename to frontend/app/shared/components/history.component.html diff --git a/src/Squidex/app/shared/components/history.component.scss b/frontend/app/shared/components/history.component.scss similarity index 100% rename from src/Squidex/app/shared/components/history.component.scss rename to frontend/app/shared/components/history.component.scss diff --git a/src/Squidex/app/shared/components/history.component.ts b/frontend/app/shared/components/history.component.ts similarity index 100% rename from src/Squidex/app/shared/components/history.component.ts rename to frontend/app/shared/components/history.component.ts diff --git a/src/Squidex/app/shared/components/language-selector.component.html b/frontend/app/shared/components/language-selector.component.html similarity index 100% rename from src/Squidex/app/shared/components/language-selector.component.html rename to frontend/app/shared/components/language-selector.component.html diff --git a/src/Squidex/app/shared/components/language-selector.component.scss b/frontend/app/shared/components/language-selector.component.scss similarity index 100% rename from src/Squidex/app/shared/components/language-selector.component.scss rename to frontend/app/shared/components/language-selector.component.scss diff --git a/src/Squidex/app/shared/components/language-selector.component.ts b/frontend/app/shared/components/language-selector.component.ts similarity index 100% rename from src/Squidex/app/shared/components/language-selector.component.ts rename to frontend/app/shared/components/language-selector.component.ts diff --git a/src/Squidex/app/shared/components/markdown-editor.component.html b/frontend/app/shared/components/markdown-editor.component.html similarity index 100% rename from src/Squidex/app/shared/components/markdown-editor.component.html rename to frontend/app/shared/components/markdown-editor.component.html diff --git a/src/Squidex/app/shared/components/markdown-editor.component.scss b/frontend/app/shared/components/markdown-editor.component.scss similarity index 100% rename from src/Squidex/app/shared/components/markdown-editor.component.scss rename to frontend/app/shared/components/markdown-editor.component.scss diff --git a/src/Squidex/app/shared/components/markdown-editor.component.ts b/frontend/app/shared/components/markdown-editor.component.ts similarity index 100% rename from src/Squidex/app/shared/components/markdown-editor.component.ts rename to frontend/app/shared/components/markdown-editor.component.ts diff --git a/src/Squidex/app/shared/components/pipes.ts b/frontend/app/shared/components/pipes.ts similarity index 100% rename from src/Squidex/app/shared/components/pipes.ts rename to frontend/app/shared/components/pipes.ts diff --git a/src/Squidex/app/shared/components/queries/filter-comparison.component.html b/frontend/app/shared/components/queries/filter-comparison.component.html similarity index 100% rename from src/Squidex/app/shared/components/queries/filter-comparison.component.html rename to frontend/app/shared/components/queries/filter-comparison.component.html diff --git a/src/Squidex/app/shared/components/queries/filter-comparison.component.scss b/frontend/app/shared/components/queries/filter-comparison.component.scss similarity index 100% rename from src/Squidex/app/shared/components/queries/filter-comparison.component.scss rename to frontend/app/shared/components/queries/filter-comparison.component.scss diff --git a/src/Squidex/app/shared/components/queries/filter-comparison.component.ts b/frontend/app/shared/components/queries/filter-comparison.component.ts similarity index 100% rename from src/Squidex/app/shared/components/queries/filter-comparison.component.ts rename to frontend/app/shared/components/queries/filter-comparison.component.ts diff --git a/src/Squidex/app/shared/components/queries/filter-logical.component.html b/frontend/app/shared/components/queries/filter-logical.component.html similarity index 100% rename from src/Squidex/app/shared/components/queries/filter-logical.component.html rename to frontend/app/shared/components/queries/filter-logical.component.html diff --git a/src/Squidex/app/shared/components/queries/filter-logical.component.scss b/frontend/app/shared/components/queries/filter-logical.component.scss similarity index 100% rename from src/Squidex/app/shared/components/queries/filter-logical.component.scss rename to frontend/app/shared/components/queries/filter-logical.component.scss diff --git a/src/Squidex/app/shared/components/queries/filter-logical.component.ts b/frontend/app/shared/components/queries/filter-logical.component.ts similarity index 100% rename from src/Squidex/app/shared/components/queries/filter-logical.component.ts rename to frontend/app/shared/components/queries/filter-logical.component.ts diff --git a/src/Squidex/app/shared/components/queries/filter-node.component.ts b/frontend/app/shared/components/queries/filter-node.component.ts similarity index 100% rename from src/Squidex/app/shared/components/queries/filter-node.component.ts rename to frontend/app/shared/components/queries/filter-node.component.ts diff --git a/src/Squidex/app/shared/components/queries/query.component.ts b/frontend/app/shared/components/queries/query.component.ts similarity index 100% rename from src/Squidex/app/shared/components/queries/query.component.ts rename to frontend/app/shared/components/queries/query.component.ts diff --git a/src/Squidex/app/shared/components/queries/sorting.component.ts b/frontend/app/shared/components/queries/sorting.component.ts similarity index 100% rename from src/Squidex/app/shared/components/queries/sorting.component.ts rename to frontend/app/shared/components/queries/sorting.component.ts diff --git a/src/Squidex/app/shared/components/references-dropdown.component.ts b/frontend/app/shared/components/references-dropdown.component.ts similarity index 100% rename from src/Squidex/app/shared/components/references-dropdown.component.ts rename to frontend/app/shared/components/references-dropdown.component.ts diff --git a/src/Squidex/app/shared/components/rich-editor.component.html b/frontend/app/shared/components/rich-editor.component.html similarity index 100% rename from src/Squidex/app/shared/components/rich-editor.component.html rename to frontend/app/shared/components/rich-editor.component.html diff --git a/src/Squidex/app/shared/components/rich-editor.component.scss b/frontend/app/shared/components/rich-editor.component.scss similarity index 100% rename from src/Squidex/app/shared/components/rich-editor.component.scss rename to frontend/app/shared/components/rich-editor.component.scss diff --git a/src/Squidex/app/shared/components/rich-editor.component.ts b/frontend/app/shared/components/rich-editor.component.ts similarity index 100% rename from src/Squidex/app/shared/components/rich-editor.component.ts rename to frontend/app/shared/components/rich-editor.component.ts diff --git a/src/Squidex/app/shared/components/saved-queries.component.ts b/frontend/app/shared/components/saved-queries.component.ts similarity index 100% rename from src/Squidex/app/shared/components/saved-queries.component.ts rename to frontend/app/shared/components/saved-queries.component.ts diff --git a/src/Squidex/app/shared/components/schema-category.component.html b/frontend/app/shared/components/schema-category.component.html similarity index 100% rename from src/Squidex/app/shared/components/schema-category.component.html rename to frontend/app/shared/components/schema-category.component.html diff --git a/src/Squidex/app/shared/components/schema-category.component.scss b/frontend/app/shared/components/schema-category.component.scss similarity index 100% rename from src/Squidex/app/shared/components/schema-category.component.scss rename to frontend/app/shared/components/schema-category.component.scss diff --git a/src/Squidex/app/shared/components/schema-category.component.ts b/frontend/app/shared/components/schema-category.component.ts similarity index 100% rename from src/Squidex/app/shared/components/schema-category.component.ts rename to frontend/app/shared/components/schema-category.component.ts diff --git a/src/Squidex/app/shared/components/search-form.component.html b/frontend/app/shared/components/search-form.component.html similarity index 100% rename from src/Squidex/app/shared/components/search-form.component.html rename to frontend/app/shared/components/search-form.component.html diff --git a/src/Squidex/app/shared/components/search-form.component.scss b/frontend/app/shared/components/search-form.component.scss similarity index 100% rename from src/Squidex/app/shared/components/search-form.component.scss rename to frontend/app/shared/components/search-form.component.scss diff --git a/src/Squidex/app/shared/components/search-form.component.ts b/frontend/app/shared/components/search-form.component.ts similarity index 100% rename from src/Squidex/app/shared/components/search-form.component.ts rename to frontend/app/shared/components/search-form.component.ts diff --git a/src/Squidex/app/shared/components/table-header.component.ts b/frontend/app/shared/components/table-header.component.ts similarity index 100% rename from src/Squidex/app/shared/components/table-header.component.ts rename to frontend/app/shared/components/table-header.component.ts diff --git a/src/Squidex/app/shared/declarations.ts b/frontend/app/shared/declarations.ts similarity index 100% rename from src/Squidex/app/shared/declarations.ts rename to frontend/app/shared/declarations.ts diff --git a/src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts b/frontend/app/shared/guards/app-must-exist.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts rename to frontend/app/shared/guards/app-must-exist.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/app-must-exist.guard.ts b/frontend/app/shared/guards/app-must-exist.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/app-must-exist.guard.ts rename to frontend/app/shared/guards/app-must-exist.guard.ts diff --git a/src/Squidex/app/shared/guards/content-must-exist.guard.spec.ts b/frontend/app/shared/guards/content-must-exist.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/content-must-exist.guard.spec.ts rename to frontend/app/shared/guards/content-must-exist.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/content-must-exist.guard.ts b/frontend/app/shared/guards/content-must-exist.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/content-must-exist.guard.ts rename to frontend/app/shared/guards/content-must-exist.guard.ts diff --git a/src/Squidex/app/shared/guards/load-apps.guard.spec.ts b/frontend/app/shared/guards/load-apps.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/load-apps.guard.spec.ts rename to frontend/app/shared/guards/load-apps.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/load-apps.guard.ts b/frontend/app/shared/guards/load-apps.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/load-apps.guard.ts rename to frontend/app/shared/guards/load-apps.guard.ts diff --git a/src/Squidex/app/shared/guards/load-languages.guard.spec.ts b/frontend/app/shared/guards/load-languages.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/load-languages.guard.spec.ts rename to frontend/app/shared/guards/load-languages.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/load-languages.guard.ts b/frontend/app/shared/guards/load-languages.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/load-languages.guard.ts rename to frontend/app/shared/guards/load-languages.guard.ts diff --git a/src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts b/frontend/app/shared/guards/must-be-authenticated.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts rename to frontend/app/shared/guards/must-be-authenticated.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/must-be-authenticated.guard.ts b/frontend/app/shared/guards/must-be-authenticated.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/must-be-authenticated.guard.ts rename to frontend/app/shared/guards/must-be-authenticated.guard.ts diff --git a/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts b/frontend/app/shared/guards/must-be-not-authenticated.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts rename to frontend/app/shared/guards/must-be-not-authenticated.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.ts b/frontend/app/shared/guards/must-be-not-authenticated.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/must-be-not-authenticated.guard.ts rename to frontend/app/shared/guards/must-be-not-authenticated.guard.ts diff --git a/src/Squidex/app/shared/guards/schema-must-exist-published.guard.spec.ts b/frontend/app/shared/guards/schema-must-exist-published.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/schema-must-exist-published.guard.spec.ts rename to frontend/app/shared/guards/schema-must-exist-published.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/schema-must-exist-published.guard.ts b/frontend/app/shared/guards/schema-must-exist-published.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/schema-must-exist-published.guard.ts rename to frontend/app/shared/guards/schema-must-exist-published.guard.ts diff --git a/src/Squidex/app/shared/guards/schema-must-exist.guard.spec.ts b/frontend/app/shared/guards/schema-must-exist.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/schema-must-exist.guard.spec.ts rename to frontend/app/shared/guards/schema-must-exist.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/schema-must-exist.guard.ts b/frontend/app/shared/guards/schema-must-exist.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/schema-must-exist.guard.ts rename to frontend/app/shared/guards/schema-must-exist.guard.ts diff --git a/src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts b/frontend/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts rename to frontend/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.ts b/frontend/app/shared/guards/schema-must-not-be-singleton.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.ts rename to frontend/app/shared/guards/schema-must-not-be-singleton.guard.ts diff --git a/src/Squidex/app/shared/guards/unset-app.guard.spec.ts b/frontend/app/shared/guards/unset-app.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/unset-app.guard.spec.ts rename to frontend/app/shared/guards/unset-app.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/unset-app.guard.ts b/frontend/app/shared/guards/unset-app.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/unset-app.guard.ts rename to frontend/app/shared/guards/unset-app.guard.ts diff --git a/src/Squidex/app/shared/guards/unset-content.guard.spec.ts b/frontend/app/shared/guards/unset-content.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/unset-content.guard.spec.ts rename to frontend/app/shared/guards/unset-content.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/unset-content.guard.ts b/frontend/app/shared/guards/unset-content.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/unset-content.guard.ts rename to frontend/app/shared/guards/unset-content.guard.ts diff --git a/src/Squidex/app/shared/index.ts b/frontend/app/shared/index.ts similarity index 100% rename from src/Squidex/app/shared/index.ts rename to frontend/app/shared/index.ts diff --git a/src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts b/frontend/app/shared/interceptors/auth.interceptor.spec.ts similarity index 100% rename from src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts rename to frontend/app/shared/interceptors/auth.interceptor.spec.ts diff --git a/src/Squidex/app/shared/interceptors/auth.interceptor.ts b/frontend/app/shared/interceptors/auth.interceptor.ts similarity index 100% rename from src/Squidex/app/shared/interceptors/auth.interceptor.ts rename to frontend/app/shared/interceptors/auth.interceptor.ts diff --git a/src/Squidex/app/shared/internal.ts b/frontend/app/shared/internal.ts similarity index 100% rename from src/Squidex/app/shared/internal.ts rename to frontend/app/shared/internal.ts diff --git a/src/Squidex/app/shared/module.ts b/frontend/app/shared/module.ts similarity index 100% rename from src/Squidex/app/shared/module.ts rename to frontend/app/shared/module.ts diff --git a/src/Squidex/app/shared/services/app-languages.service.spec.ts b/frontend/app/shared/services/app-languages.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/app-languages.service.spec.ts rename to frontend/app/shared/services/app-languages.service.spec.ts diff --git a/src/Squidex/app/shared/services/app-languages.service.ts b/frontend/app/shared/services/app-languages.service.ts similarity index 100% rename from src/Squidex/app/shared/services/app-languages.service.ts rename to frontend/app/shared/services/app-languages.service.ts diff --git a/src/Squidex/app/shared/services/apps.service.spec.ts b/frontend/app/shared/services/apps.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/apps.service.spec.ts rename to frontend/app/shared/services/apps.service.spec.ts diff --git a/src/Squidex/app/shared/services/apps.service.ts b/frontend/app/shared/services/apps.service.ts similarity index 100% rename from src/Squidex/app/shared/services/apps.service.ts rename to frontend/app/shared/services/apps.service.ts diff --git a/src/Squidex/app/shared/services/assets.service.spec.ts b/frontend/app/shared/services/assets.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/assets.service.spec.ts rename to frontend/app/shared/services/assets.service.spec.ts diff --git a/src/Squidex/app/shared/services/assets.service.ts b/frontend/app/shared/services/assets.service.ts similarity index 100% rename from src/Squidex/app/shared/services/assets.service.ts rename to frontend/app/shared/services/assets.service.ts diff --git a/src/Squidex/app/shared/services/auth.service.ts b/frontend/app/shared/services/auth.service.ts similarity index 100% rename from src/Squidex/app/shared/services/auth.service.ts rename to frontend/app/shared/services/auth.service.ts diff --git a/src/Squidex/app/shared/services/autosave.service.spec.ts b/frontend/app/shared/services/autosave.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/autosave.service.spec.ts rename to frontend/app/shared/services/autosave.service.spec.ts diff --git a/src/Squidex/app/shared/services/autosave.service.ts b/frontend/app/shared/services/autosave.service.ts similarity index 100% rename from src/Squidex/app/shared/services/autosave.service.ts rename to frontend/app/shared/services/autosave.service.ts diff --git a/src/Squidex/app/shared/services/backups.service.spec.ts b/frontend/app/shared/services/backups.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/backups.service.spec.ts rename to frontend/app/shared/services/backups.service.spec.ts diff --git a/src/Squidex/app/shared/services/backups.service.ts b/frontend/app/shared/services/backups.service.ts similarity index 100% rename from src/Squidex/app/shared/services/backups.service.ts rename to frontend/app/shared/services/backups.service.ts diff --git a/src/Squidex/app/shared/services/clients.service.spec.ts b/frontend/app/shared/services/clients.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/clients.service.spec.ts rename to frontend/app/shared/services/clients.service.spec.ts diff --git a/src/Squidex/app/shared/services/clients.service.ts b/frontend/app/shared/services/clients.service.ts similarity index 100% rename from src/Squidex/app/shared/services/clients.service.ts rename to frontend/app/shared/services/clients.service.ts diff --git a/src/Squidex/app/shared/services/comments.service.spec.ts b/frontend/app/shared/services/comments.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/comments.service.spec.ts rename to frontend/app/shared/services/comments.service.spec.ts diff --git a/src/Squidex/app/shared/services/comments.service.ts b/frontend/app/shared/services/comments.service.ts similarity index 100% rename from src/Squidex/app/shared/services/comments.service.ts rename to frontend/app/shared/services/comments.service.ts diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/frontend/app/shared/services/contents.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/contents.service.spec.ts rename to frontend/app/shared/services/contents.service.spec.ts diff --git a/src/Squidex/app/shared/services/contents.service.ts b/frontend/app/shared/services/contents.service.ts similarity index 100% rename from src/Squidex/app/shared/services/contents.service.ts rename to frontend/app/shared/services/contents.service.ts diff --git a/src/Squidex/app/shared/services/contributors.service.spec.ts b/frontend/app/shared/services/contributors.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/contributors.service.spec.ts rename to frontend/app/shared/services/contributors.service.spec.ts diff --git a/src/Squidex/app/shared/services/contributors.service.ts b/frontend/app/shared/services/contributors.service.ts similarity index 100% rename from src/Squidex/app/shared/services/contributors.service.ts rename to frontend/app/shared/services/contributors.service.ts diff --git a/src/Squidex/app/shared/services/graphql.service.spec.ts b/frontend/app/shared/services/graphql.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/graphql.service.spec.ts rename to frontend/app/shared/services/graphql.service.spec.ts diff --git a/src/Squidex/app/shared/services/graphql.service.ts b/frontend/app/shared/services/graphql.service.ts similarity index 100% rename from src/Squidex/app/shared/services/graphql.service.ts rename to frontend/app/shared/services/graphql.service.ts diff --git a/src/Squidex/app/shared/services/help.service.spec.ts b/frontend/app/shared/services/help.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/help.service.spec.ts rename to frontend/app/shared/services/help.service.spec.ts diff --git a/src/Squidex/app/shared/services/help.service.ts b/frontend/app/shared/services/help.service.ts similarity index 100% rename from src/Squidex/app/shared/services/help.service.ts rename to frontend/app/shared/services/help.service.ts diff --git a/src/Squidex/app/shared/services/history.service.spec.ts b/frontend/app/shared/services/history.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/history.service.spec.ts rename to frontend/app/shared/services/history.service.spec.ts diff --git a/src/Squidex/app/shared/services/history.service.ts b/frontend/app/shared/services/history.service.ts similarity index 100% rename from src/Squidex/app/shared/services/history.service.ts rename to frontend/app/shared/services/history.service.ts diff --git a/src/Squidex/app/shared/services/languages.service.spec.ts b/frontend/app/shared/services/languages.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/languages.service.spec.ts rename to frontend/app/shared/services/languages.service.spec.ts diff --git a/src/Squidex/app/shared/services/languages.service.ts b/frontend/app/shared/services/languages.service.ts similarity index 100% rename from src/Squidex/app/shared/services/languages.service.ts rename to frontend/app/shared/services/languages.service.ts diff --git a/src/Squidex/app/shared/services/news.service.spec.ts b/frontend/app/shared/services/news.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/news.service.spec.ts rename to frontend/app/shared/services/news.service.spec.ts diff --git a/src/Squidex/app/shared/services/news.service.ts b/frontend/app/shared/services/news.service.ts similarity index 100% rename from src/Squidex/app/shared/services/news.service.ts rename to frontend/app/shared/services/news.service.ts diff --git a/src/Squidex/app/shared/services/patterns.service.spec.ts b/frontend/app/shared/services/patterns.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/patterns.service.spec.ts rename to frontend/app/shared/services/patterns.service.spec.ts diff --git a/src/Squidex/app/shared/services/patterns.service.ts b/frontend/app/shared/services/patterns.service.ts similarity index 100% rename from src/Squidex/app/shared/services/patterns.service.ts rename to frontend/app/shared/services/patterns.service.ts diff --git a/src/Squidex/app/shared/services/plans.service.spec.ts b/frontend/app/shared/services/plans.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/plans.service.spec.ts rename to frontend/app/shared/services/plans.service.spec.ts diff --git a/src/Squidex/app/shared/services/plans.service.ts b/frontend/app/shared/services/plans.service.ts similarity index 100% rename from src/Squidex/app/shared/services/plans.service.ts rename to frontend/app/shared/services/plans.service.ts diff --git a/src/Squidex/app/shared/services/roles.service.spec.ts b/frontend/app/shared/services/roles.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/roles.service.spec.ts rename to frontend/app/shared/services/roles.service.spec.ts diff --git a/src/Squidex/app/shared/services/roles.service.ts b/frontend/app/shared/services/roles.service.ts similarity index 100% rename from src/Squidex/app/shared/services/roles.service.ts rename to frontend/app/shared/services/roles.service.ts diff --git a/src/Squidex/app/shared/services/rules.service.spec.ts b/frontend/app/shared/services/rules.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/rules.service.spec.ts rename to frontend/app/shared/services/rules.service.spec.ts diff --git a/src/Squidex/app/shared/services/rules.service.ts b/frontend/app/shared/services/rules.service.ts similarity index 100% rename from src/Squidex/app/shared/services/rules.service.ts rename to frontend/app/shared/services/rules.service.ts diff --git a/src/Squidex/app/shared/services/schemas.service.spec.ts b/frontend/app/shared/services/schemas.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/schemas.service.spec.ts rename to frontend/app/shared/services/schemas.service.spec.ts diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/frontend/app/shared/services/schemas.service.ts similarity index 100% rename from src/Squidex/app/shared/services/schemas.service.ts rename to frontend/app/shared/services/schemas.service.ts diff --git a/src/Squidex/app/shared/services/schemas.types.ts b/frontend/app/shared/services/schemas.types.ts similarity index 100% rename from src/Squidex/app/shared/services/schemas.types.ts rename to frontend/app/shared/services/schemas.types.ts diff --git a/src/Squidex/app/shared/services/translations.service.spec.ts b/frontend/app/shared/services/translations.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/translations.service.spec.ts rename to frontend/app/shared/services/translations.service.spec.ts diff --git a/src/Squidex/app/shared/services/translations.service.ts b/frontend/app/shared/services/translations.service.ts similarity index 100% rename from src/Squidex/app/shared/services/translations.service.ts rename to frontend/app/shared/services/translations.service.ts diff --git a/src/Squidex/app/shared/services/ui.service.spec.ts b/frontend/app/shared/services/ui.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/ui.service.spec.ts rename to frontend/app/shared/services/ui.service.spec.ts diff --git a/src/Squidex/app/shared/services/ui.service.ts b/frontend/app/shared/services/ui.service.ts similarity index 100% rename from src/Squidex/app/shared/services/ui.service.ts rename to frontend/app/shared/services/ui.service.ts diff --git a/src/Squidex/app/shared/services/usages.service.spec.ts b/frontend/app/shared/services/usages.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/usages.service.spec.ts rename to frontend/app/shared/services/usages.service.spec.ts diff --git a/src/Squidex/app/shared/services/usages.service.ts b/frontend/app/shared/services/usages.service.ts similarity index 100% rename from src/Squidex/app/shared/services/usages.service.ts rename to frontend/app/shared/services/usages.service.ts diff --git a/src/Squidex/app/shared/services/users-provider.service.spec.ts b/frontend/app/shared/services/users-provider.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/users-provider.service.spec.ts rename to frontend/app/shared/services/users-provider.service.spec.ts diff --git a/src/Squidex/app/shared/services/users-provider.service.ts b/frontend/app/shared/services/users-provider.service.ts similarity index 100% rename from src/Squidex/app/shared/services/users-provider.service.ts rename to frontend/app/shared/services/users-provider.service.ts diff --git a/src/Squidex/app/shared/services/users.service.spec.ts b/frontend/app/shared/services/users.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/users.service.spec.ts rename to frontend/app/shared/services/users.service.spec.ts diff --git a/src/Squidex/app/shared/services/users.service.ts b/frontend/app/shared/services/users.service.ts similarity index 100% rename from src/Squidex/app/shared/services/users.service.ts rename to frontend/app/shared/services/users.service.ts diff --git a/src/Squidex/app/shared/services/workflows.service.spec.ts b/frontend/app/shared/services/workflows.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/workflows.service.spec.ts rename to frontend/app/shared/services/workflows.service.spec.ts diff --git a/src/Squidex/app/shared/services/workflows.service.ts b/frontend/app/shared/services/workflows.service.ts similarity index 100% rename from src/Squidex/app/shared/services/workflows.service.ts rename to frontend/app/shared/services/workflows.service.ts diff --git a/src/Squidex/app/shared/state/_test-helpers.ts b/frontend/app/shared/state/_test-helpers.ts similarity index 100% rename from src/Squidex/app/shared/state/_test-helpers.ts rename to frontend/app/shared/state/_test-helpers.ts diff --git a/src/Squidex/app/shared/state/apps.forms.ts b/frontend/app/shared/state/apps.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/apps.forms.ts rename to frontend/app/shared/state/apps.forms.ts diff --git a/src/Squidex/app/shared/state/apps.state.spec.ts b/frontend/app/shared/state/apps.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/apps.state.spec.ts rename to frontend/app/shared/state/apps.state.spec.ts diff --git a/src/Squidex/app/shared/state/apps.state.ts b/frontend/app/shared/state/apps.state.ts similarity index 100% rename from src/Squidex/app/shared/state/apps.state.ts rename to frontend/app/shared/state/apps.state.ts diff --git a/src/Squidex/app/shared/state/asset-uploader.state.spec.ts b/frontend/app/shared/state/asset-uploader.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/asset-uploader.state.spec.ts rename to frontend/app/shared/state/asset-uploader.state.spec.ts diff --git a/src/Squidex/app/shared/state/asset-uploader.state.ts b/frontend/app/shared/state/asset-uploader.state.ts similarity index 100% rename from src/Squidex/app/shared/state/asset-uploader.state.ts rename to frontend/app/shared/state/asset-uploader.state.ts diff --git a/src/Squidex/app/shared/state/assets.forms.ts b/frontend/app/shared/state/assets.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/assets.forms.ts rename to frontend/app/shared/state/assets.forms.ts diff --git a/src/Squidex/app/shared/state/assets.state.spec.ts b/frontend/app/shared/state/assets.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/assets.state.spec.ts rename to frontend/app/shared/state/assets.state.spec.ts diff --git a/src/Squidex/app/shared/state/assets.state.ts b/frontend/app/shared/state/assets.state.ts similarity index 100% rename from src/Squidex/app/shared/state/assets.state.ts rename to frontend/app/shared/state/assets.state.ts diff --git a/src/Squidex/app/shared/state/backups.forms.ts b/frontend/app/shared/state/backups.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/backups.forms.ts rename to frontend/app/shared/state/backups.forms.ts diff --git a/src/Squidex/app/shared/state/backups.state.spec.ts b/frontend/app/shared/state/backups.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/backups.state.spec.ts rename to frontend/app/shared/state/backups.state.spec.ts diff --git a/src/Squidex/app/shared/state/backups.state.ts b/frontend/app/shared/state/backups.state.ts similarity index 100% rename from src/Squidex/app/shared/state/backups.state.ts rename to frontend/app/shared/state/backups.state.ts diff --git a/src/Squidex/app/shared/state/clients.forms.ts b/frontend/app/shared/state/clients.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/clients.forms.ts rename to frontend/app/shared/state/clients.forms.ts diff --git a/src/Squidex/app/shared/state/clients.state.spec.ts b/frontend/app/shared/state/clients.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/clients.state.spec.ts rename to frontend/app/shared/state/clients.state.spec.ts diff --git a/src/Squidex/app/shared/state/clients.state.ts b/frontend/app/shared/state/clients.state.ts similarity index 100% rename from src/Squidex/app/shared/state/clients.state.ts rename to frontend/app/shared/state/clients.state.ts diff --git a/src/Squidex/app/shared/state/comments.form.ts b/frontend/app/shared/state/comments.form.ts similarity index 100% rename from src/Squidex/app/shared/state/comments.form.ts rename to frontend/app/shared/state/comments.form.ts diff --git a/src/Squidex/app/shared/state/comments.state.spec.ts b/frontend/app/shared/state/comments.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/comments.state.spec.ts rename to frontend/app/shared/state/comments.state.spec.ts diff --git a/src/Squidex/app/shared/state/comments.state.ts b/frontend/app/shared/state/comments.state.ts similarity index 100% rename from src/Squidex/app/shared/state/comments.state.ts rename to frontend/app/shared/state/comments.state.ts diff --git a/src/Squidex/app/shared/state/contents.forms.spec.ts b/frontend/app/shared/state/contents.forms.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/contents.forms.spec.ts rename to frontend/app/shared/state/contents.forms.spec.ts diff --git a/src/Squidex/app/shared/state/contents.forms.ts b/frontend/app/shared/state/contents.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/contents.forms.ts rename to frontend/app/shared/state/contents.forms.ts diff --git a/src/Squidex/app/shared/state/contents.state.ts b/frontend/app/shared/state/contents.state.ts similarity index 100% rename from src/Squidex/app/shared/state/contents.state.ts rename to frontend/app/shared/state/contents.state.ts diff --git a/src/Squidex/app/shared/state/contributors.forms.ts b/frontend/app/shared/state/contributors.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/contributors.forms.ts rename to frontend/app/shared/state/contributors.forms.ts diff --git a/src/Squidex/app/shared/state/contributors.state.spec.ts b/frontend/app/shared/state/contributors.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/contributors.state.spec.ts rename to frontend/app/shared/state/contributors.state.spec.ts diff --git a/src/Squidex/app/shared/state/contributors.state.ts b/frontend/app/shared/state/contributors.state.ts similarity index 100% rename from src/Squidex/app/shared/state/contributors.state.ts rename to frontend/app/shared/state/contributors.state.ts diff --git a/src/Squidex/app/shared/state/languages.forms.ts b/frontend/app/shared/state/languages.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/languages.forms.ts rename to frontend/app/shared/state/languages.forms.ts diff --git a/src/Squidex/app/shared/state/languages.state.spec.ts b/frontend/app/shared/state/languages.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/languages.state.spec.ts rename to frontend/app/shared/state/languages.state.spec.ts diff --git a/src/Squidex/app/shared/state/languages.state.ts b/frontend/app/shared/state/languages.state.ts similarity index 100% rename from src/Squidex/app/shared/state/languages.state.ts rename to frontend/app/shared/state/languages.state.ts diff --git a/src/Squidex/app/shared/state/patterns.forms.ts b/frontend/app/shared/state/patterns.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/patterns.forms.ts rename to frontend/app/shared/state/patterns.forms.ts diff --git a/src/Squidex/app/shared/state/patterns.state.spec.ts b/frontend/app/shared/state/patterns.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/patterns.state.spec.ts rename to frontend/app/shared/state/patterns.state.spec.ts diff --git a/src/Squidex/app/shared/state/patterns.state.ts b/frontend/app/shared/state/patterns.state.ts similarity index 100% rename from src/Squidex/app/shared/state/patterns.state.ts rename to frontend/app/shared/state/patterns.state.ts diff --git a/src/Squidex/app/shared/state/plans.state.spec.ts b/frontend/app/shared/state/plans.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/plans.state.spec.ts rename to frontend/app/shared/state/plans.state.spec.ts diff --git a/src/Squidex/app/shared/state/plans.state.ts b/frontend/app/shared/state/plans.state.ts similarity index 100% rename from src/Squidex/app/shared/state/plans.state.ts rename to frontend/app/shared/state/plans.state.ts diff --git a/src/Squidex/app/shared/state/queries.spec.ts b/frontend/app/shared/state/queries.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/queries.spec.ts rename to frontend/app/shared/state/queries.spec.ts diff --git a/src/Squidex/app/shared/state/queries.ts b/frontend/app/shared/state/queries.ts similarity index 100% rename from src/Squidex/app/shared/state/queries.ts rename to frontend/app/shared/state/queries.ts diff --git a/src/Squidex/app/shared/state/query.ts b/frontend/app/shared/state/query.ts similarity index 100% rename from src/Squidex/app/shared/state/query.ts rename to frontend/app/shared/state/query.ts diff --git a/src/Squidex/app/shared/state/roles.forms.ts b/frontend/app/shared/state/roles.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/roles.forms.ts rename to frontend/app/shared/state/roles.forms.ts diff --git a/src/Squidex/app/shared/state/roles.state.spec.ts b/frontend/app/shared/state/roles.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/roles.state.spec.ts rename to frontend/app/shared/state/roles.state.spec.ts diff --git a/src/Squidex/app/shared/state/roles.state.ts b/frontend/app/shared/state/roles.state.ts similarity index 100% rename from src/Squidex/app/shared/state/roles.state.ts rename to frontend/app/shared/state/roles.state.ts diff --git a/src/Squidex/app/shared/state/rule-events.state.spec.ts b/frontend/app/shared/state/rule-events.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/rule-events.state.spec.ts rename to frontend/app/shared/state/rule-events.state.spec.ts diff --git a/src/Squidex/app/shared/state/rule-events.state.ts b/frontend/app/shared/state/rule-events.state.ts similarity index 100% rename from src/Squidex/app/shared/state/rule-events.state.ts rename to frontend/app/shared/state/rule-events.state.ts diff --git a/src/Squidex/app/shared/state/rules.state.spec.ts b/frontend/app/shared/state/rules.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/rules.state.spec.ts rename to frontend/app/shared/state/rules.state.spec.ts diff --git a/src/Squidex/app/shared/state/rules.state.ts b/frontend/app/shared/state/rules.state.ts similarity index 100% rename from src/Squidex/app/shared/state/rules.state.ts rename to frontend/app/shared/state/rules.state.ts diff --git a/src/Squidex/app/shared/state/schema-tag-converter.ts b/frontend/app/shared/state/schema-tag-converter.ts similarity index 100% rename from src/Squidex/app/shared/state/schema-tag-converter.ts rename to frontend/app/shared/state/schema-tag-converter.ts diff --git a/src/Squidex/app/shared/state/schemas.forms.ts b/frontend/app/shared/state/schemas.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/schemas.forms.ts rename to frontend/app/shared/state/schemas.forms.ts diff --git a/src/Squidex/app/shared/state/schemas.state.spec.ts b/frontend/app/shared/state/schemas.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/schemas.state.spec.ts rename to frontend/app/shared/state/schemas.state.spec.ts diff --git a/src/Squidex/app/shared/state/schemas.state.ts b/frontend/app/shared/state/schemas.state.ts similarity index 100% rename from src/Squidex/app/shared/state/schemas.state.ts rename to frontend/app/shared/state/schemas.state.ts diff --git a/src/Squidex/app/shared/state/ui.state.spec.ts b/frontend/app/shared/state/ui.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/ui.state.spec.ts rename to frontend/app/shared/state/ui.state.spec.ts diff --git a/src/Squidex/app/shared/state/ui.state.ts b/frontend/app/shared/state/ui.state.ts similarity index 100% rename from src/Squidex/app/shared/state/ui.state.ts rename to frontend/app/shared/state/ui.state.ts diff --git a/src/Squidex/app/shared/state/workflows.forms.ts b/frontend/app/shared/state/workflows.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/workflows.forms.ts rename to frontend/app/shared/state/workflows.forms.ts diff --git a/src/Squidex/app/shared/state/workflows.state.spec.ts b/frontend/app/shared/state/workflows.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/workflows.state.spec.ts rename to frontend/app/shared/state/workflows.state.spec.ts diff --git a/src/Squidex/app/shared/state/workflows.state.ts b/frontend/app/shared/state/workflows.state.ts similarity index 100% rename from src/Squidex/app/shared/state/workflows.state.ts rename to frontend/app/shared/state/workflows.state.ts diff --git a/src/Squidex/app/shared/utils/messages.ts b/frontend/app/shared/utils/messages.ts similarity index 100% rename from src/Squidex/app/shared/utils/messages.ts rename to frontend/app/shared/utils/messages.ts diff --git a/src/Squidex/app/shell/declarations.ts b/frontend/app/shell/declarations.ts similarity index 100% rename from src/Squidex/app/shell/declarations.ts rename to frontend/app/shell/declarations.ts diff --git a/src/Squidex/app/shell/index.ts b/frontend/app/shell/index.ts similarity index 100% rename from src/Squidex/app/shell/index.ts rename to frontend/app/shell/index.ts diff --git a/src/Squidex/app/shell/module.ts b/frontend/app/shell/module.ts similarity index 100% rename from src/Squidex/app/shell/module.ts rename to frontend/app/shell/module.ts diff --git a/src/Squidex/app/shell/pages/app/app-area.component.html b/frontend/app/shell/pages/app/app-area.component.html similarity index 100% rename from src/Squidex/app/shell/pages/app/app-area.component.html rename to frontend/app/shell/pages/app/app-area.component.html diff --git a/src/Squidex/app/shell/pages/app/app-area.component.scss b/frontend/app/shell/pages/app/app-area.component.scss similarity index 100% rename from src/Squidex/app/shell/pages/app/app-area.component.scss rename to frontend/app/shell/pages/app/app-area.component.scss diff --git a/src/Squidex/app/shell/pages/app/app-area.component.ts b/frontend/app/shell/pages/app/app-area.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/app/app-area.component.ts rename to frontend/app/shell/pages/app/app-area.component.ts diff --git a/src/Squidex/app/shell/pages/app/left-menu.component.html b/frontend/app/shell/pages/app/left-menu.component.html similarity index 100% rename from src/Squidex/app/shell/pages/app/left-menu.component.html rename to frontend/app/shell/pages/app/left-menu.component.html diff --git a/src/Squidex/app/shell/pages/app/left-menu.component.scss b/frontend/app/shell/pages/app/left-menu.component.scss similarity index 100% rename from src/Squidex/app/shell/pages/app/left-menu.component.scss rename to frontend/app/shell/pages/app/left-menu.component.scss diff --git a/src/Squidex/app/shell/pages/app/left-menu.component.ts b/frontend/app/shell/pages/app/left-menu.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/app/left-menu.component.ts rename to frontend/app/shell/pages/app/left-menu.component.ts diff --git a/src/Squidex/app/shell/pages/forbidden/forbidden-page.component.ts b/frontend/app/shell/pages/forbidden/forbidden-page.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/forbidden/forbidden-page.component.ts rename to frontend/app/shell/pages/forbidden/forbidden-page.component.ts diff --git a/src/Squidex/app/shell/pages/home/home-page.component.html b/frontend/app/shell/pages/home/home-page.component.html similarity index 100% rename from src/Squidex/app/shell/pages/home/home-page.component.html rename to frontend/app/shell/pages/home/home-page.component.html diff --git a/src/Squidex/app/shell/pages/home/home-page.component.scss b/frontend/app/shell/pages/home/home-page.component.scss similarity index 100% rename from src/Squidex/app/shell/pages/home/home-page.component.scss rename to frontend/app/shell/pages/home/home-page.component.scss diff --git a/src/Squidex/app/shell/pages/home/home-page.component.ts b/frontend/app/shell/pages/home/home-page.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/home/home-page.component.ts rename to frontend/app/shell/pages/home/home-page.component.ts diff --git a/src/Squidex/app/shell/pages/internal/apps-menu.component.html b/frontend/app/shell/pages/internal/apps-menu.component.html similarity index 100% rename from src/Squidex/app/shell/pages/internal/apps-menu.component.html rename to frontend/app/shell/pages/internal/apps-menu.component.html diff --git a/src/Squidex/app/shell/pages/internal/apps-menu.component.scss b/frontend/app/shell/pages/internal/apps-menu.component.scss similarity index 100% rename from src/Squidex/app/shell/pages/internal/apps-menu.component.scss rename to frontend/app/shell/pages/internal/apps-menu.component.scss diff --git a/src/Squidex/app/shell/pages/internal/apps-menu.component.ts b/frontend/app/shell/pages/internal/apps-menu.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/internal/apps-menu.component.ts rename to frontend/app/shell/pages/internal/apps-menu.component.ts diff --git a/src/Squidex/app/shell/pages/internal/internal-area.component.html b/frontend/app/shell/pages/internal/internal-area.component.html similarity index 100% rename from src/Squidex/app/shell/pages/internal/internal-area.component.html rename to frontend/app/shell/pages/internal/internal-area.component.html diff --git a/src/Squidex/app/shell/pages/internal/internal-area.component.scss b/frontend/app/shell/pages/internal/internal-area.component.scss similarity index 100% rename from src/Squidex/app/shell/pages/internal/internal-area.component.scss rename to frontend/app/shell/pages/internal/internal-area.component.scss diff --git a/src/Squidex/app/shell/pages/internal/internal-area.component.ts b/frontend/app/shell/pages/internal/internal-area.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/internal/internal-area.component.ts rename to frontend/app/shell/pages/internal/internal-area.component.ts diff --git a/src/Squidex/app/shell/pages/internal/profile-menu.component.html b/frontend/app/shell/pages/internal/profile-menu.component.html similarity index 100% rename from src/Squidex/app/shell/pages/internal/profile-menu.component.html rename to frontend/app/shell/pages/internal/profile-menu.component.html diff --git a/src/Squidex/app/shell/pages/internal/profile-menu.component.scss b/frontend/app/shell/pages/internal/profile-menu.component.scss similarity index 100% rename from src/Squidex/app/shell/pages/internal/profile-menu.component.scss rename to frontend/app/shell/pages/internal/profile-menu.component.scss diff --git a/src/Squidex/app/shell/pages/internal/profile-menu.component.ts b/frontend/app/shell/pages/internal/profile-menu.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/internal/profile-menu.component.ts rename to frontend/app/shell/pages/internal/profile-menu.component.ts diff --git a/src/Squidex/app/shell/pages/login/login-page.component.ts b/frontend/app/shell/pages/login/login-page.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/login/login-page.component.ts rename to frontend/app/shell/pages/login/login-page.component.ts diff --git a/src/Squidex/app/shell/pages/logout/logout-page.component.ts b/frontend/app/shell/pages/logout/logout-page.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/logout/logout-page.component.ts rename to frontend/app/shell/pages/logout/logout-page.component.ts diff --git a/src/Squidex/app/shell/pages/not-found/not-found-page.component.ts b/frontend/app/shell/pages/not-found/not-found-page.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/not-found/not-found-page.component.ts rename to frontend/app/shell/pages/not-found/not-found-page.component.ts diff --git a/src/Squidex/app/shims.ts b/frontend/app/shims.ts similarity index 100% rename from src/Squidex/app/shims.ts rename to frontend/app/shims.ts diff --git a/src/Squidex/app/theme/_bootstrap-vars.scss b/frontend/app/theme/_bootstrap-vars.scss similarity index 100% rename from src/Squidex/app/theme/_bootstrap-vars.scss rename to frontend/app/theme/_bootstrap-vars.scss diff --git a/src/Squidex/app/theme/_bootstrap.scss b/frontend/app/theme/_bootstrap.scss similarity index 100% rename from src/Squidex/app/theme/_bootstrap.scss rename to frontend/app/theme/_bootstrap.scss diff --git a/src/Squidex/app/theme/_common.scss b/frontend/app/theme/_common.scss similarity index 100% rename from src/Squidex/app/theme/_common.scss rename to frontend/app/theme/_common.scss diff --git a/src/Squidex/app/theme/_forms.scss b/frontend/app/theme/_forms.scss similarity index 100% rename from src/Squidex/app/theme/_forms.scss rename to frontend/app/theme/_forms.scss diff --git a/src/Squidex/app/theme/_lists.scss b/frontend/app/theme/_lists.scss similarity index 100% rename from src/Squidex/app/theme/_lists.scss rename to frontend/app/theme/_lists.scss diff --git a/src/Squidex/app/theme/_mixins.scss b/frontend/app/theme/_mixins.scss similarity index 100% rename from src/Squidex/app/theme/_mixins.scss rename to frontend/app/theme/_mixins.scss diff --git a/src/Squidex/app/theme/_panels.scss b/frontend/app/theme/_panels.scss similarity index 100% rename from src/Squidex/app/theme/_panels.scss rename to frontend/app/theme/_panels.scss diff --git a/src/Squidex/app/theme/_static.scss b/frontend/app/theme/_static.scss similarity index 100% rename from src/Squidex/app/theme/_static.scss rename to frontend/app/theme/_static.scss diff --git a/src/Squidex/app/theme/_vars.scss b/frontend/app/theme/_vars.scss similarity index 100% rename from src/Squidex/app/theme/_vars.scss rename to frontend/app/theme/_vars.scss diff --git a/src/Squidex/app/theme/icomoon/demo-files/demo.css b/frontend/app/theme/icomoon/demo-files/demo.css similarity index 100% rename from src/Squidex/app/theme/icomoon/demo-files/demo.css rename to frontend/app/theme/icomoon/demo-files/demo.css diff --git a/src/Squidex/app/theme/icomoon/demo-files/demo.js b/frontend/app/theme/icomoon/demo-files/demo.js similarity index 100% rename from src/Squidex/app/theme/icomoon/demo-files/demo.js rename to frontend/app/theme/icomoon/demo-files/demo.js diff --git a/src/Squidex/app/theme/icomoon/demo.html b/frontend/app/theme/icomoon/demo.html similarity index 100% rename from src/Squidex/app/theme/icomoon/demo.html rename to frontend/app/theme/icomoon/demo.html diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.eot b/frontend/app/theme/icomoon/fonts/icomoon.eot similarity index 100% rename from src/Squidex/app/theme/icomoon/fonts/icomoon.eot rename to frontend/app/theme/icomoon/fonts/icomoon.eot diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.svg b/frontend/app/theme/icomoon/fonts/icomoon.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/fonts/icomoon.svg rename to frontend/app/theme/icomoon/fonts/icomoon.svg diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf b/frontend/app/theme/icomoon/fonts/icomoon.ttf similarity index 100% rename from src/Squidex/app/theme/icomoon/fonts/icomoon.ttf rename to frontend/app/theme/icomoon/fonts/icomoon.ttf diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.woff b/frontend/app/theme/icomoon/fonts/icomoon.woff similarity index 100% rename from src/Squidex/app/theme/icomoon/fonts/icomoon.woff rename to frontend/app/theme/icomoon/fonts/icomoon.woff diff --git a/src/Squidex/app/theme/icomoon/icons/action-Algolia.svg b/frontend/app/theme/icomoon/icons/action-Algolia.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/action-Algolia.svg rename to frontend/app/theme/icomoon/icons/action-Algolia.svg diff --git a/src/Squidex/app/theme/icomoon/icons/action-Fastly.svg b/frontend/app/theme/icomoon/icons/action-Fastly.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/action-Fastly.svg rename to frontend/app/theme/icomoon/icons/action-Fastly.svg diff --git a/src/Squidex/app/theme/icomoon/icons/activity.svg b/frontend/app/theme/icomoon/icons/activity.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/activity.svg rename to frontend/app/theme/icomoon/icons/activity.svg diff --git a/src/Squidex/app/theme/icomoon/icons/add-app.svg b/frontend/app/theme/icomoon/icons/add-app.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/add-app.svg rename to frontend/app/theme/icomoon/icons/add-app.svg diff --git a/src/Squidex/app/theme/icomoon/icons/add.svg b/frontend/app/theme/icomoon/icons/add.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/add.svg rename to frontend/app/theme/icomoon/icons/add.svg diff --git a/src/Squidex/app/theme/icomoon/icons/api.svg b/frontend/app/theme/icomoon/icons/api.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/api.svg rename to frontend/app/theme/icomoon/icons/api.svg diff --git a/src/Squidex/app/theme/icomoon/icons/assets.svg b/frontend/app/theme/icomoon/icons/assets.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/assets.svg rename to frontend/app/theme/icomoon/icons/assets.svg diff --git a/src/Squidex/app/theme/icomoon/icons/caret-bottom.svg b/frontend/app/theme/icomoon/icons/caret-bottom.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/caret-bottom.svg rename to frontend/app/theme/icomoon/icons/caret-bottom.svg diff --git a/src/Squidex/app/theme/icomoon/icons/caret-top.svg b/frontend/app/theme/icomoon/icons/caret-top.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/caret-top.svg rename to frontend/app/theme/icomoon/icons/caret-top.svg diff --git a/src/Squidex/app/theme/icomoon/icons/check-circle-filled.svg b/frontend/app/theme/icomoon/icons/check-circle-filled.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/check-circle-filled.svg rename to frontend/app/theme/icomoon/icons/check-circle-filled.svg diff --git a/src/Squidex/app/theme/icomoon/icons/check-circle.svg b/frontend/app/theme/icomoon/icons/check-circle.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/check-circle.svg rename to frontend/app/theme/icomoon/icons/check-circle.svg diff --git a/src/Squidex/app/theme/icomoon/icons/client.svg b/frontend/app/theme/icomoon/icons/client.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/client.svg rename to frontend/app/theme/icomoon/icons/client.svg diff --git a/src/Squidex/app/theme/icomoon/icons/close.svg b/frontend/app/theme/icomoon/icons/close.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/close.svg rename to frontend/app/theme/icomoon/icons/close.svg diff --git a/src/Squidex/app/theme/icomoon/icons/contents.svg b/frontend/app/theme/icomoon/icons/contents.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/contents.svg rename to frontend/app/theme/icomoon/icons/contents.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Checkbox.svg b/frontend/app/theme/icomoon/icons/control-Checkbox.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Checkbox.svg rename to frontend/app/theme/icomoon/icons/control-Checkbox.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Checkboxes.svg b/frontend/app/theme/icomoon/icons/control-Checkboxes.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Checkboxes.svg rename to frontend/app/theme/icomoon/icons/control-Checkboxes.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Date.svg b/frontend/app/theme/icomoon/icons/control-Date.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Date.svg rename to frontend/app/theme/icomoon/icons/control-Date.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-DateTime.svg b/frontend/app/theme/icomoon/icons/control-DateTime.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-DateTime.svg rename to frontend/app/theme/icomoon/icons/control-DateTime.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Dropdown.svg b/frontend/app/theme/icomoon/icons/control-Dropdown.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Dropdown.svg rename to frontend/app/theme/icomoon/icons/control-Dropdown.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Html.svg b/frontend/app/theme/icomoon/icons/control-Html.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Html.svg rename to frontend/app/theme/icomoon/icons/control-Html.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Input.svg b/frontend/app/theme/icomoon/icons/control-Input.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Input.svg rename to frontend/app/theme/icomoon/icons/control-Input.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Markdown.svg b/frontend/app/theme/icomoon/icons/control-Markdown.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Markdown.svg rename to frontend/app/theme/icomoon/icons/control-Markdown.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Radio.svg b/frontend/app/theme/icomoon/icons/control-Radio.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Radio.svg rename to frontend/app/theme/icomoon/icons/control-Radio.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-RichText.svg b/frontend/app/theme/icomoon/icons/control-RichText.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-RichText.svg rename to frontend/app/theme/icomoon/icons/control-RichText.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Slug.svg b/frontend/app/theme/icomoon/icons/control-Slug.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Slug.svg rename to frontend/app/theme/icomoon/icons/control-Slug.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Tags.svg b/frontend/app/theme/icomoon/icons/control-Tags.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Tags.svg rename to frontend/app/theme/icomoon/icons/control-Tags.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-TextArea.svg b/frontend/app/theme/icomoon/icons/control-TextArea.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-TextArea.svg rename to frontend/app/theme/icomoon/icons/control-TextArea.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Toggle.svg b/frontend/app/theme/icomoon/icons/control-Toggle.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Toggle.svg rename to frontend/app/theme/icomoon/icons/control-Toggle.svg diff --git a/src/Squidex/app/theme/icomoon/icons/copy.svg b/frontend/app/theme/icomoon/icons/copy.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/copy.svg rename to frontend/app/theme/icomoon/icons/copy.svg diff --git a/src/Squidex/app/theme/icomoon/icons/dashboard-api.svg b/frontend/app/theme/icomoon/icons/dashboard-api.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/dashboard-api.svg rename to frontend/app/theme/icomoon/icons/dashboard-api.svg diff --git a/src/Squidex/app/theme/icomoon/icons/dashboard-feedback.svg b/frontend/app/theme/icomoon/icons/dashboard-feedback.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/dashboard-feedback.svg rename to frontend/app/theme/icomoon/icons/dashboard-feedback.svg diff --git a/src/Squidex/app/theme/icomoon/icons/dashboard-github.svg b/frontend/app/theme/icomoon/icons/dashboard-github.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/dashboard-github.svg rename to frontend/app/theme/icomoon/icons/dashboard-github.svg diff --git a/src/Squidex/app/theme/icomoon/icons/dashboard-schema.svg b/frontend/app/theme/icomoon/icons/dashboard-schema.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/dashboard-schema.svg rename to frontend/app/theme/icomoon/icons/dashboard-schema.svg diff --git a/src/Squidex/app/theme/icomoon/icons/dashboard.svg b/frontend/app/theme/icomoon/icons/dashboard.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/dashboard.svg rename to frontend/app/theme/icomoon/icons/dashboard.svg diff --git a/src/Squidex/app/theme/icomoon/icons/delete-filled.svg b/frontend/app/theme/icomoon/icons/delete-filled.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/delete-filled.svg rename to frontend/app/theme/icomoon/icons/delete-filled.svg diff --git a/src/Squidex/app/theme/icomoon/icons/delete.svg b/frontend/app/theme/icomoon/icons/delete.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/delete.svg rename to frontend/app/theme/icomoon/icons/delete.svg diff --git a/src/Squidex/app/theme/icomoon/icons/document-delete.svg b/frontend/app/theme/icomoon/icons/document-delete.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/document-delete.svg rename to frontend/app/theme/icomoon/icons/document-delete.svg diff --git a/src/Squidex/app/theme/icomoon/icons/document-disable.svg b/frontend/app/theme/icomoon/icons/document-disable.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/document-disable.svg rename to frontend/app/theme/icomoon/icons/document-disable.svg diff --git a/src/Squidex/app/theme/icomoon/icons/document-lock.svg b/frontend/app/theme/icomoon/icons/document-lock.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/document-lock.svg rename to frontend/app/theme/icomoon/icons/document-lock.svg diff --git a/src/Squidex/app/theme/icomoon/icons/document-publish.svg b/frontend/app/theme/icomoon/icons/document-publish.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/document-publish.svg rename to frontend/app/theme/icomoon/icons/document-publish.svg diff --git a/src/Squidex/app/theme/icomoon/icons/document-unpublish.svg b/frontend/app/theme/icomoon/icons/document-unpublish.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/document-unpublish.svg rename to frontend/app/theme/icomoon/icons/document-unpublish.svg diff --git a/src/Squidex/app/theme/icomoon/icons/drag.svg b/frontend/app/theme/icomoon/icons/drag.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/drag.svg rename to frontend/app/theme/icomoon/icons/drag.svg diff --git a/src/Squidex/app/theme/icomoon/icons/fastly.svg b/frontend/app/theme/icomoon/icons/fastly.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/fastly.svg rename to frontend/app/theme/icomoon/icons/fastly.svg diff --git a/src/Squidex/app/theme/icomoon/icons/filter.svg b/frontend/app/theme/icomoon/icons/filter.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/filter.svg rename to frontend/app/theme/icomoon/icons/filter.svg diff --git a/src/Squidex/app/theme/icomoon/icons/help.svg b/frontend/app/theme/icomoon/icons/help.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/help.svg rename to frontend/app/theme/icomoon/icons/help.svg diff --git a/src/Squidex/app/theme/icomoon/icons/hide-all.svg b/frontend/app/theme/icomoon/icons/hide-all.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/hide-all.svg rename to frontend/app/theme/icomoon/icons/hide-all.svg diff --git a/src/Squidex/app/theme/icomoon/icons/hide.svg b/frontend/app/theme/icomoon/icons/hide.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/hide.svg rename to frontend/app/theme/icomoon/icons/hide.svg diff --git a/src/Squidex/app/theme/icomoon/icons/json.svg b/frontend/app/theme/icomoon/icons/json.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/json.svg rename to frontend/app/theme/icomoon/icons/json.svg diff --git a/src/Squidex/app/theme/icomoon/icons/location.svg b/frontend/app/theme/icomoon/icons/location.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/location.svg rename to frontend/app/theme/icomoon/icons/location.svg diff --git a/src/Squidex/app/theme/icomoon/icons/logo.svg b/frontend/app/theme/icomoon/icons/logo.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/logo.svg rename to frontend/app/theme/icomoon/icons/logo.svg diff --git a/src/Squidex/app/theme/icomoon/icons/media.svg b/frontend/app/theme/icomoon/icons/media.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/media.svg rename to frontend/app/theme/icomoon/icons/media.svg diff --git a/src/Squidex/app/theme/icomoon/icons/more.svg b/frontend/app/theme/icomoon/icons/more.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/more.svg rename to frontend/app/theme/icomoon/icons/more.svg diff --git a/src/Squidex/app/theme/icomoon/icons/multiple-content.svg b/frontend/app/theme/icomoon/icons/multiple-content.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/multiple-content.svg rename to frontend/app/theme/icomoon/icons/multiple-content.svg diff --git a/src/Squidex/app/theme/icomoon/icons/orleans.svg b/frontend/app/theme/icomoon/icons/orleans.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/orleans.svg rename to frontend/app/theme/icomoon/icons/orleans.svg diff --git a/src/Squidex/app/theme/icomoon/icons/pencil.svg b/frontend/app/theme/icomoon/icons/pencil.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/pencil.svg rename to frontend/app/theme/icomoon/icons/pencil.svg diff --git a/src/Squidex/app/theme/icomoon/icons/reference.svg b/frontend/app/theme/icomoon/icons/reference.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/reference.svg rename to frontend/app/theme/icomoon/icons/reference.svg diff --git a/src/Squidex/app/theme/icomoon/icons/schemas.svg b/frontend/app/theme/icomoon/icons/schemas.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/schemas.svg rename to frontend/app/theme/icomoon/icons/schemas.svg diff --git a/src/Squidex/app/theme/icomoon/icons/search.svg b/frontend/app/theme/icomoon/icons/search.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/search.svg rename to frontend/app/theme/icomoon/icons/search.svg diff --git a/src/Squidex/app/theme/icomoon/icons/settings.svg b/frontend/app/theme/icomoon/icons/settings.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/settings.svg rename to frontend/app/theme/icomoon/icons/settings.svg diff --git a/src/Squidex/app/theme/icomoon/icons/show-all.svg b/frontend/app/theme/icomoon/icons/show-all.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/show-all.svg rename to frontend/app/theme/icomoon/icons/show-all.svg diff --git a/src/Squidex/app/theme/icomoon/icons/show.svg b/frontend/app/theme/icomoon/icons/show.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/show.svg rename to frontend/app/theme/icomoon/icons/show.svg diff --git a/src/Squidex/app/theme/icomoon/icons/single-content.svg b/frontend/app/theme/icomoon/icons/single-content.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/single-content.svg rename to frontend/app/theme/icomoon/icons/single-content.svg diff --git a/src/Squidex/app/theme/icomoon/icons/type-Array.svg b/frontend/app/theme/icomoon/icons/type-Array.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/type-Array.svg rename to frontend/app/theme/icomoon/icons/type-Array.svg diff --git a/src/Squidex/app/theme/icomoon/icons/type-Boolean.svg b/frontend/app/theme/icomoon/icons/type-Boolean.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/type-Boolean.svg rename to frontend/app/theme/icomoon/icons/type-Boolean.svg diff --git a/src/Squidex/app/theme/icomoon/icons/type-DateTime.svg b/frontend/app/theme/icomoon/icons/type-DateTime.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/type-DateTime.svg rename to frontend/app/theme/icomoon/icons/type-DateTime.svg diff --git a/src/Squidex/app/theme/icomoon/icons/type-Number.svg b/frontend/app/theme/icomoon/icons/type-Number.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/type-Number.svg rename to frontend/app/theme/icomoon/icons/type-Number.svg diff --git a/src/Squidex/app/theme/icomoon/icons/type-String.svg b/frontend/app/theme/icomoon/icons/type-String.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/type-String.svg rename to frontend/app/theme/icomoon/icons/type-String.svg diff --git a/src/Squidex/app/theme/icomoon/icons/type-Tags.svg b/frontend/app/theme/icomoon/icons/type-Tags.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/type-Tags.svg rename to frontend/app/theme/icomoon/icons/type-Tags.svg diff --git a/src/Squidex/app/theme/icomoon/icons/user-o.svg b/frontend/app/theme/icomoon/icons/user-o.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/user-o.svg rename to frontend/app/theme/icomoon/icons/user-o.svg diff --git a/src/Squidex/app/theme/icomoon/icons/user.svg b/frontend/app/theme/icomoon/icons/user.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/user.svg rename to frontend/app/theme/icomoon/icons/user.svg diff --git a/src/Squidex/app/theme/icomoon/icons/webhooks.svg b/frontend/app/theme/icomoon/icons/webhooks.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/webhooks.svg rename to frontend/app/theme/icomoon/icons/webhooks.svg diff --git a/src/Squidex/app/theme/icomoon/selection.json b/frontend/app/theme/icomoon/selection.json similarity index 100% rename from src/Squidex/app/theme/icomoon/selection.json rename to frontend/app/theme/icomoon/selection.json diff --git a/src/Squidex/app/theme/icomoon/style.css b/frontend/app/theme/icomoon/style.css similarity index 100% rename from src/Squidex/app/theme/icomoon/style.css rename to frontend/app/theme/icomoon/style.css diff --git a/src/Squidex/app/theme/theme.scss b/frontend/app/theme/theme.scss similarity index 100% rename from src/Squidex/app/theme/theme.scss rename to frontend/app/theme/theme.scss diff --git a/src/Squidex/karma.conf.js b/frontend/karma.conf.js similarity index 100% rename from src/Squidex/karma.conf.js rename to frontend/karma.conf.js diff --git a/src/Squidex/karma.coverage.conf.js b/frontend/karma.coverage.conf.js similarity index 100% rename from src/Squidex/karma.coverage.conf.js rename to frontend/karma.coverage.conf.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 000000000..b125a6e1b --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,17012 @@ +{ + "name": "squidex", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@angular-devkit/build-optimizer": { + "version": "0.803.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.803.8.tgz", + "integrity": "sha512-UiMxl1wI3acqIoRkC0WA0qpab+ni6SlCaB4UIwfD1H/FdzU80P04AIUuJS7StxjbwVkVtA05kcfgmqzP8yBMVg==", + "dev": true, + "requires": { + "loader-utils": "1.2.3", + "source-map": "0.7.3", + "tslib": "1.10.0", + "typescript": "3.5.3", + "webpack-sources": "1.4.3" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + } + } + }, + "@angular-devkit/core": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-8.3.8.tgz", + "integrity": "sha512-HwlMRr6qANwhOJS+5rGgQ2lmP4nj2C4cbUc0LlA09Cdbq0RnDquUFVqHF6h81FUKFW1D5qDehWYHNOVq8+gTkQ==", + "dev": true, + "requires": { + "ajv": "6.10.2", + "fast-json-stable-stringify": "2.0.0", + "magic-string": "0.25.3", + "rxjs": "6.4.0", + "source-map": "0.7.3" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "magic-string": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.3.tgz", + "integrity": "sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "rxjs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", + "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + } + } + }, + "@angular/animations": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-8.2.9.tgz", + "integrity": "sha512-l30AF0d9P5okTPM1wieUHgcnDyGSNvyaBcxXSOkT790wAP2v5zs7VrKq9Lm+ICu4Nkx07KrOr5XLUHhqsg3VXA==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/cdk": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-8.2.3.tgz", + "integrity": "sha512-ZwO5Sn720RA2YvBqud0JAHkZXjmjxM0yNzCO8RVtRE9i8Gl26Wk0j0nQeJkVm4zwv2QO8MwbKUKGTMt8evsokA==", + "requires": { + "parse5": "^5.0.0", + "tslib": "^1.7.1" + } + }, + "@angular/common": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-8.2.9.tgz", + "integrity": "sha512-76WDU1USlI5vAzqCJ3gxCQGuu57aJEggNk/xoWmQEXipiFTFBh2wSKn/dE6Txr/q3COTPIcrmb9OCeal5kQPIA==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/compiler": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-8.2.9.tgz", + "integrity": "sha512-oQho19DnOhEDNerCOGuGK95tcZ2oy4dSA5SykJmmniRnZzPM2++bJD32qJehXHy1K+3hv2zN9x7HPhqT3ljT6g==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/compiler-cli": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-8.2.9.tgz", + "integrity": "sha512-tqGBKPf3SRYNEGGJbmjom//U/eAjnecDhGUw6o+VkYE/wxYd9pPcLmcEwwyXBpIPJAsN8RsjTikPuH0gcNE8bw==", + "dev": true, + "requires": { + "canonical-path": "1.0.0", + "chokidar": "^2.1.1", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.7.2", + "magic-string": "^0.25.0", + "minimist": "^1.2.0", + "reflect-metadata": "^0.1.2", + "source-map": "^0.6.1", + "tslib": "^1.9.0", + "yargs": "13.1.0" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + } + } + }, + "@angular/core": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-8.2.9.tgz", + "integrity": "sha512-GpHAuLOlN9iioELCQBmAsjETTUCyFgVUI3LXwh3e63jnpd+ZuuZcZbjfTYhtgYVNMetn7cVEO6p88eb7qvpUWQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/forms": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-8.2.9.tgz", + "integrity": "sha512-kAdBuApC9PPOdPI8BmNhxCraAkXGbX/PkVan8pQ5xdumvgGqvVjbJvLaUSbJROPtgCRlQyiEDrHFd4gk/WU76A==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/http": { + "version": "7.2.15", + "resolved": "https://registry.npmjs.org/@angular/http/-/http-7.2.15.tgz", + "integrity": "sha512-TR7PEdmLWNIre3Zn8lvyb4lSrvPUJhKLystLnp4hBMcWsJqq5iK8S3bnlR4viZ9HMlf7bW7+Hm4SI6aB3tdUtw==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/platform-browser": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-8.2.9.tgz", + "integrity": "sha512-k3aNZy0OTqGn7HlHHV52QF6ZAP/VlQhWGD2u5e1dWIWMq39kdkdSCNu5tiuAf5hIzMBiSQ0tjnuVWA4MuDBYIQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/platform-browser-dynamic": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-8.2.9.tgz", + "integrity": "sha512-GbE4TUy4n/a8yp8fLWwdG/QnjUPZZ8VufItZ7GvOpoyknzegvka111dLctvMoPzSAsrKyShL6cryuyDC5PShUA==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/platform-server": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-8.2.9.tgz", + "integrity": "sha512-rr6h82+DdUGhpsF3WT3eLk5itjZDXe7SiNtRGHkPj+yTyFAxuTKA3cX0N7LWsGGIFax+s1vQhMreV4YcyHKGPQ==", + "requires": { + "domino": "^2.1.2", + "tslib": "^1.9.0", + "xhr2": "^0.1.4" + } + }, + "@angular/router": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-8.2.9.tgz", + "integrity": "sha512-4P60CWNB/jxGjDBEuYN0Jobt76QlebAQeFBTDswRVwRlq/WJT4QhL3a8AVIRsHn9bQII0LUt/ZQBBPxn7h9lSA==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/generator": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz", + "integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==", + "dev": true, + "requires": { + "@babel/types": "^7.5.5", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + }, + "dependencies": { + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", + "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==", + "dev": true + }, + "@babel/template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", + "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/traverse": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz", + "integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.5.5", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.5.5", + "@babel/types": "^7.5.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz", + "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + } + } + }, + "@ngtools/webpack": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-8.3.8.tgz", + "integrity": "sha512-jLN4/Abue+Ro/K2SF0TpHOXnFHGuaHQ4aL6QG++moZXavBxRdc2E+PDjtuaMaS1llLHs5C5GX+Ve9ueEFhWoeQ==", + "dev": true, + "requires": { + "@angular-devkit/core": "8.3.8", + "enhanced-resolve": "4.1.0", + "rxjs": "6.4.0", + "tree-kill": "1.2.1", + "webpack-sources": "1.4.3" + }, + "dependencies": { + "rxjs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", + "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + } + } + }, + "@types/core-js": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@types/core-js/-/core-js-2.5.2.tgz", + "integrity": "sha512-+NPqjXgyA02xTHKJDeDca9u8Zr42ts6jhdND4C3PrPeQ35RJa0dmfAedXW7a9K4N1QcBbuWI1nSfGK4r1eVFCQ==", + "dev": true + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/jasmine": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.4.2.tgz", + "integrity": "sha512-SaSSGOzwUnBEn64c+HTyVTJhRf8F1CXZLnxYx2ww3UrgGBmEEw38RSux2l3fYiT9brVLP67DU5omWA6V9OHI5Q==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, + "@types/marked": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.6.5.tgz", + "integrity": "sha512-6kBKf64aVfx93UJrcyEZ+OBM5nGv4RLsI6sR1Ar34bpgvGVRoyTgpxn4ZmtxOM5aDTAaaznYuYUH8bUX3Nk3YA==", + "dev": true + }, + "@types/mersenne-twister": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/mersenne-twister/-/mersenne-twister-1.1.2.tgz", + "integrity": "sha512-7KMIfSkMpaVExbzJRLUXHMO4hkFWbbspHPREk8I6pBxiNN+3+l6eAEClMCIPIo2KjCkR0rjYfXppr6+wKdTwpA==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/mousetrap": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.0.tgz", + "integrity": "sha512-Jn2cF8X6RAMiSmJaATGjf2r3GzIfpZQpvnQhKprQ5sAbMaNXc7hc9sA2XHdMl3bEMEQhTV79JVW7n4Pgg7sjtg==", + "dev": true + }, + "@types/node": { + "version": "12.7.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.11.tgz", + "integrity": "sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "dev": true + }, + "@types/q": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", + "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", + "dev": true + }, + "@types/react": { + "version": "16.9.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.5.tgz", + "integrity": "sha512-jQ12VMiFOWYlp+j66dghOWcmDDwhca0bnlcTxS4Qz/fh5gi6wpaZDthPEu/Gc/YlAuO87vbiUXL8qKstFvuOaA==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, + "@types/react-dom": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.1.tgz", + "integrity": "sha512-1S/akvkKr63qIUWVu5IKYou2P9fHLb/P2VAwyxVV85JGaGZTcUniMiTuIqM3lXFB25ej6h+CYEQ27ERVwi6eGA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/sortablejs": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.7.2.tgz", + "integrity": "sha512-yIxpbtlfhaFi2QyuUK54XcmzDWZf5i11CgTrMO4Vh+sKKZthonizkTcqhADeHdngDNTDVUCYfIcfIvpZRAZY+A==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", + "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", + "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", + "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", + "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", + "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", + "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", + "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "mamacro": "^0.0.3" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", + "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", + "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", + "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", + "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", + "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", + "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/helper-wasm-section": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-opt": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", + "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", + "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", + "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", + "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/floating-point-hex-parser": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-code-frame": "1.8.5", + "@webassemblyjs/helper-fsm": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", + "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", + "dev": true + }, + "aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "dependencies": { + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + } + } + }, + "ajv": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", + "integrity": "sha1-06/3jpJ3VJdx2vAWTP9ISCt1T8Y=", + "dev": true, + "requires": { + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0", + "uri-js": "^3.0.2" + } + }, + "ajv-errors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.0.tgz", + "integrity": "sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk=", + "dev": true + }, + "ajv-keywords": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "dev": true + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "angular2-chartjs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/angular2-chartjs/-/angular2-chartjs-0.5.1.tgz", + "integrity": "sha512-bxEVxVEv7llMcgwuc9jlc5KmuOEngT7ZlUyCddmsXwQQAahrTeNgFJ1Nc1SVQnq2fl2d8efh6m70DqF5beiA+A==", + "requires": { + "chart.js": "^2.3.0" + } + }, + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + }, + "dependencies": { + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "dev": true, + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", + "dev": true + } + } + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "app-root-path": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz", + "integrity": "sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA==", + "dev": true + }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aria-query": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", + "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=" + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=" + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=" + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "ast-types": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", + "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=", + "dev": true + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", + "dev": true + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } + } + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "dev": true + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "axobject-query": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", + "integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7" + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + }, + "dependencies": { + "jsesc": { + "version": "1.3.0", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "requires": { + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "regenerator-runtime": "^0.10.5" + }, + "dependencies": { + "core-js": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", + "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" + } + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "core-js": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", + "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + } + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "dev": true + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "dev": true, + "requires": { + "callsite": "1.0.0" + } + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=" + }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", + "dev": true + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "bluebird": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.0.tgz", + "integrity": "sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg==", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "bootstrap": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz", + "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.0.tgz", + "integrity": "sha512-9rGNDtnj+HaahxiVV38Gn8n8Lr8REKsel68v1sPFfIGEK6uSXTY3h9acgiT1dZVtOOUtifo/Dn8daDQ5dUgVsA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000989", + "electron-to-chromium": "^1.3.247", + "node-releases": "^1.1.29" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "cacache": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz", + "integrity": "sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w==", + "dev": true, + "requires": { + "chownr": "^1.1.2", + "figgy-pudding": "^3.5.1", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.2", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "minipass": "^3.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "p-map": "^3.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^2.7.1", + "ssri": "^7.0.0", + "unique-filename": "^1.1.1" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", + "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + }, + "dependencies": { + "callsites": { + "version": "2.0.0", + "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + } + } + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "requires": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30000998", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000998.tgz", + "integrity": "sha512-8Tj5sPZR9kMHeDD9SZXIVr5m9ofufLLCG2Y4QwQrH18GIwG+kCc+zYdlR036ZRkuKjVVetyxeAgGA1xF7XdmzQ==", + "dev": true + }, + "canonical-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/canonical-path/-/canonical-path-1.0.0.tgz", + "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chart.js": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.7.2.tgz", + "integrity": "sha512-90wl3V9xRZ8tnMvMlpcW+0Yg13BelsGS9P9t0ClaDxv/hdypHDr/YAGf+728m11P5ljwyB0ZHfPKCapZFqSqYA==", + "requires": { + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" + } + }, + "chartjs-color": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.2.0.tgz", + "integrity": "sha1-hKL7dVeH7YXDndbdjHsdiEKbrq4=", + "requires": { + "chartjs-color-string": "^0.5.0", + "color-convert": "^0.5.3" + } + }, + "chartjs-color-string": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz", + "integrity": "sha512-amWNvCOXlOUYxZVDSa0YOab5K/lmEhbFNKI55PWc4mlv28BDzA7zaoQTGxSBgJMHIW+hGX8YUrvw/FH4LyhwSQ==", + "requires": { + "color-name": "^1.0.0" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "chownr": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", + "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "circular-dependency-plugin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz", + "integrity": "sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw==", + "dev": true + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "clean-css": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", + "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", + "dev": true, + "requires": { + "source-map": "0.5.x" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dev": true, + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "codelyzer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-5.1.2.tgz", + "integrity": "sha512-1z7mtpwxcz5uUqq0HLO0ifj/tz2dWEmeaK+8c5TEZXAwwVxrjjg0118ODCOCCOcpfYaaEHxStNCaWVYo9FUPXw==", + "dev": true, + "requires": { + "app-root-path": "^2.2.1", + "aria-query": "^3.0.0", + "axobject-query": "^2.0.2", + "css-selector-tokenizer": "^0.7.1", + "cssauron": "^1.4.0", + "damerau-levenshtein": "^1.0.4", + "semver-dsl": "^1.0.1", + "source-map": "^0.5.7", + "sprintf-js": "^1.1.2" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + } + } + }, + "codemirror": { + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.49.0.tgz", + "integrity": "sha512-Hyzr0HToBdZpLBN9dYFO/KlJAsKH37/cXVHPAqa+imml0R92tb9AkmsvjnXL+SluEvjjdfkDgRjc65NG5jnMYA==" + }, + "codemirror-graphql": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-0.8.3.tgz", + "integrity": "sha512-ZipSnPXFKDMThfvfTKTAt1dQmuGctVNann8hTZg6017+vwOcGpIqCuQIZLRDw/Y3zZfCyydRARHgbSydSCXpow==", + "requires": { + "graphql-language-service-interface": "^1.3.2", + "graphql-language-service-parser": "^1.2.2" + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", + "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", + "dev": true, + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + } + } + }, + "color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.16.0.tgz", + "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "compare-versions": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.5.1.tgz", + "integrity": "sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg==", + "dev": true + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, + "compressible": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", + "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "dev": true, + "requires": { + "mime-db": ">= 1.40.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "copy-to-clipboard": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz", + "integrity": "sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==", + "requires": { + "toggle-selection": "^1.0.6" + } + }, + "core-js": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.2.1.tgz", + "integrity": "sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "dependencies": { + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, + "cpx": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cpx/-/cpx-1.5.0.tgz", + "integrity": "sha1-GFvgGFEdhycN7czCkxceN2VauI8=", + "requires": { + "babel-runtime": "^6.9.2", + "chokidar": "^1.6.0", + "duplexer": "^0.1.1", + "glob": "^7.0.5", + "glob2base": "^0.0.12", + "minimatch": "^3.0.2", + "mkdirp": "^0.5.1", + "resolve": "^1.1.7", + "safe-buffer": "^5.0.1", + "shell-quote": "^1.6.1", + "subarg": "^1.0.0" + } + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-fetch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.2.tgz", + "integrity": "sha1-pH/09/xxLauo9qaVoRyUhEDUVyM=", + "requires": { + "node-fetch": "2.1.2", + "whatwg-fetch": "2.0.4" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "crypto-js": { + "version": "3.1.9-1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", + "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "http://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true + }, + "css-declaration-sorter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "dev": true, + "requires": { + "postcss": "^7.0.1", + "timsort": "^0.3.0" + } + }, + "css-loader": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.2.0.tgz", + "integrity": "sha512-QTF3Ud5H7DaZotgdcJjGMvyDj5F3Pn1j/sC6VBEOVp94cbwqyIBdcs/quzj4MC1BKQSrTpQznegH/5giYbhnCQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.17", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.1.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.0.0", + "schema-utils": "^2.0.0" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "schema-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", + "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "dev": true + }, + "css-selector-tokenizer": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", + "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==", + "dev": true, + "requires": { + "cssesc": "^0.1.0", + "fastparse": "^1.1.1", + "regexpu-core": "^1.0.0" + } + }, + "css-tree": { + "version": "1.0.0-alpha.33", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.33.tgz", + "integrity": "sha512-SPt57bh5nQnpsTBsx/IXbO14sRc9xXu5MtMAVuo0BaQQmyf0NupNPPSoMaqiAF5tDFafYsTkfeH4Q/HCKXkg4w==", + "dev": true, + "requires": { + "mdn-data": "2.0.4", + "source-map": "^0.5.3" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "css-unit-converter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz", + "integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=", + "dev": true + }, + "css-what": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", + "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", + "dev": true + }, + "cssauron": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", + "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", + "dev": true, + "requires": { + "through": "X.X.X" + } + }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", + "dev": true + }, + "cssnano": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", + "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "cssnano-preset-default": "^4.0.7", + "is-resolvable": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "cssnano-preset-default": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", + "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", + "dev": true, + "requires": { + "css-declaration-sorter": "^4.0.1", + "cssnano-util-raw-cache": "^4.0.1", + "postcss": "^7.0.0", + "postcss-calc": "^7.0.1", + "postcss-colormin": "^4.0.3", + "postcss-convert-values": "^4.0.1", + "postcss-discard-comments": "^4.0.2", + "postcss-discard-duplicates": "^4.0.2", + "postcss-discard-empty": "^4.0.1", + "postcss-discard-overridden": "^4.0.1", + "postcss-merge-longhand": "^4.0.11", + "postcss-merge-rules": "^4.0.3", + "postcss-minify-font-values": "^4.0.2", + "postcss-minify-gradients": "^4.0.2", + "postcss-minify-params": "^4.0.2", + "postcss-minify-selectors": "^4.0.2", + "postcss-normalize-charset": "^4.0.1", + "postcss-normalize-display-values": "^4.0.2", + "postcss-normalize-positions": "^4.0.2", + "postcss-normalize-repeat-style": "^4.0.2", + "postcss-normalize-string": "^4.0.2", + "postcss-normalize-timing-functions": "^4.0.2", + "postcss-normalize-unicode": "^4.0.1", + "postcss-normalize-url": "^4.0.1", + "postcss-normalize-whitespace": "^4.0.2", + "postcss-ordered-values": "^4.1.2", + "postcss-reduce-initial": "^4.0.3", + "postcss-reduce-transforms": "^4.0.2", + "postcss-svgo": "^4.0.2", + "postcss-unique-selectors": "^4.0.1" + } + }, + "cssnano-util-get-arguments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", + "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", + "dev": true + }, + "cssnano-util-get-match": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", + "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", + "dev": true + }, + "cssnano-util-raw-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "cssnano-util-same-parent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", + "dev": true + }, + "csso": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", + "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==", + "dev": true, + "requires": { + "css-tree": "1.0.0-alpha.29" + }, + "dependencies": { + "css-tree": { + "version": "1.0.0-alpha.29", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz", + "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==", + "dev": true, + "requires": { + "mdn-data": "~1.1.0", + "source-map": "^0.5.3" + } + }, + "mdn-data": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz", + "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "csstype": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz", + "integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==", + "dev": true + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "^0.10.9" + } + }, + "damerau-levenshtein": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz", + "integrity": "sha512-CBCRqFnpu715iPmw1KrdOrzRqbdFwQTwAWyyyYS42+iAgHCuXZ+/TdMgQkUENPomxEz9z1BEzuQU2Xw0kUuAgA==", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", + "dev": true + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.0.tgz", + "integrity": "sha512-ZbfWJq/wN1Z273o7mUSjILYqehAktR2NVoSrOukDkU9kg2v/Uv89yU4Cvz8seJeAmtN5oqiefKq8FPuXOboqLw==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + }, + "dependencies": { + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + } + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "deepmerge": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.1.1.tgz", + "integrity": "sha512-urQxA1smbLZ2cBbXbaYObM1dJ82aJ2H57A1C/Kklfh/ZN1bgH4G/n5KWhdNfOK11W98gqZfyYj7W4frJJRwA2w==", + "dev": true + }, + "default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + } + }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "dependency-graph": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.7.2.tgz", + "integrity": "sha512-KqtH4/EZdtdfWX0p6MGP9jljvxSY6msy/pRUD4jgNwVpv3v1QmNLlsB3LDSSUg79BRVSn7jI1QPRtArGABovAQ==", + "dev": true + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true + }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "dev": true, + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "dom-converter": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz", + "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=", + "dev": true, + "requires": { + "utila": "~0.3" + }, + "dependencies": { + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", + "dev": true + } + } + }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "requires": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "dev": true, + "requires": { + "domelementtype": "~1.1.1", + "entities": "~1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", + "dev": true + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", + "dev": true + }, + "domhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz", + "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domino": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.3.tgz", + "integrity": "sha512-EwjTbUv1Q/RLQOdn9k7ClHutrQcWGsfXaRQNOnM/KgK4xDBoLFEcIRFuBSxAx13Vfa63X029gXYrNFrSy+DOSg==" + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.273", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.273.tgz", + "integrity": "sha512-0kUppiHQvHEENHh+nTtvTt4eXMwcPyWmMaj73GPrSEm3ldKhmmHuOH6IjrmuW6YmyS/fpXcLvMQLNVpqRhpNWw==", + "dev": true + }, + "elliptic": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz", + "integrity": "sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "engine.io": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz", + "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.0", + "ws": "~3.3.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "engine.io-client": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.1", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~3.3.1", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "engine.io-parser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", + "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", + "dev": true, + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "enhanced-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", + "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "tapable": "^1.0.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", + "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.1.1", + "function-bind": "^1.1.1", + "has": "^1.0.1", + "is-callable": "^1.1.3", + "is-regex": "^1.0.4" + } + }, + "es-to-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "dev": true, + "requires": { + "is-callable": "^1.1.1", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.1" + } + }, + "es5-ext": { + "version": "0.10.46", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz", + "integrity": "sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw==", + "dev": true, + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "1" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "es6-templates": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/es6-templates/-/es6-templates-0.2.3.tgz", + "integrity": "sha1-XLmsn7He1usSOTQrgdeSu7QHjuQ=", + "dev": true, + "requires": { + "recast": "~0.11.12", + "through": "~2.3.6" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.14", + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true, + "requires": { + "es6-map": "^0.1.3", + "es6-weak-map": "^2.0.1", + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-2.13.1.tgz", + "integrity": "sha1-5MyPoPAJ+4KaquI4VaKTYL4fbBE=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "concat-stream": "^1.4.6", + "debug": "^2.1.1", + "doctrine": "^1.2.2", + "es6-map": "^0.1.3", + "escope": "^3.6.0", + "espree": "^3.1.6", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^1.1.1", + "glob": "^7.0.3", + "globals": "^9.2.0", + "ignore": "^3.1.2", + "imurmurhash": "^0.1.4", + "inquirer": "^0.12.0", + "is-my-json-valid": "^2.10.0", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.5.1", + "json-stable-stringify": "^1.0.0", + "levn": "^0.3.0", + "lodash": "^4.0.0", + "mkdirp": "^0.5.0", + "optionator": "^0.8.1", + "path-is-absolute": "^1.0.0", + "path-is-inside": "^1.0.1", + "pluralize": "^1.2.1", + "progress": "^1.1.8", + "require-uncached": "^1.0.2", + "shelljs": "^0.6.0", + "strip-json-comments": "~1.0.1", + "table": "^3.7.8", + "text-table": "~0.2.0", + "user-home": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "shelljs": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz", + "integrity": "sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "espree": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "dev": true, + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "eventemitter3": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", + "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==", + "dev": true + }, + "events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", + "dev": true + }, + "eventsource": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", + "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "dev": true, + "requires": { + "original": "^1.0.0" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "requires": { + "fill-range": "^2.1.0" + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastparse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", + "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=", + "dev": true + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "dev": true + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "file-entry-cache": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.3.1.tgz", + "integrity": "sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "file-loader": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.2.0.tgz", + "integrity": "sha512-+xZnaK5R8kBJrHK0/6HRlrKNamvVS5rjyuju+rnyxRGuwUJwpAMsVzUl5dz6rK8brkzjV6JpcFNjp6NqV0g1OQ==", + "dev": true, + "requires": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.0.0" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "schema-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", + "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" + }, + "fileset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", + "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", + "dev": true, + "requires": { + "glob": "^7.0.3", + "minimatch": "^3.0.3" + } + }, + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "find-cache-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.0.0.tgz", + "integrity": "sha512-t7ulV1fmbxh5G9l/492O1p5+EBbr3uwpt6odhFTMc+nWyhmbloe+ja9BZ8pIBtqFWhOmCWVjx+pTW4zDkFoclw==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.0", + "pkg-dir": "^4.1.0" + }, + "dependencies": { + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "find-index": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz", + "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=" + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "flat-cache": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", + "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", + "write": "^0.2.1" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "dev": true + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "follow-redirects": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.9.0.tgz", + "integrity": "sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A==", + "dev": true, + "requires": { + "debug": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "requires": { + "for-in": "^1.0.1" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "front-matter": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-2.1.2.tgz", + "integrity": "sha1-91mDufL0E75ljJPf172M5AePXNs=", + "dev": true, + "requires": { + "js-yaml": "^3.4.6" + } + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-minipass": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.0.0.tgz", + "integrity": "sha512-40Qz+LFXmd9tzYVnnBmZvFfvAADfUA14TXPK1s7IfElJTIZ97rA8w4Kin7Wt5JBrC3ShnnFJO/5vPjPEeJIq9A==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "bundled": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": "^2.1.0" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "optional": true + }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.1", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "^0.5.1", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "optional": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "bundled": true, + "optional": true, + "requires": { + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.2.4", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.1", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "optional": true, + "requires": { + "string-width": "^1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true, + "optional": true + } + } + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "requires": { + "globule": "^1.0.0" + } + }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "requires": { + "is-property": "^1.0.2" + } + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "^1.0.0" + } + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "^2.0.0" + } + }, + "glob2base": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", + "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", + "requires": { + "find-index": "^0.1.1" + } + }, + "global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "requires": { + "global-prefix": "^3.0.0" + }, + "dependencies": { + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + }, + "gonzales-pe-sl": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/gonzales-pe-sl/-/gonzales-pe-sl-4.2.3.tgz", + "integrity": "sha1-aoaLw4BkXxQf7rBCxvl/zHG1n+Y=", + "dev": true, + "requires": { + "minimist": "1.1.x" + }, + "dependencies": { + "minimist": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz", + "integrity": "sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=", + "dev": true + } + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "graphiql": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-0.13.2.tgz", + "integrity": "sha512-4N2HmQQpUfApS1cxrTtoZ15tnR3EW88oUiqmza6GgNQYZZfDdBGphdQlBYsKcjAB/SnIOJort+RA1dB6kf4M7Q==", + "requires": { + "codemirror": "^5.47.0", + "codemirror-graphql": "^0.8.3", + "copy-to-clipboard": "^3.2.0", + "markdown-it": "^8.4.0" + } + }, + "graphql": { + "version": "14.4.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.4.2.tgz", + "integrity": "sha512-6uQadiRgnpnSS56hdZUSvFrVcQ6OF9y6wkxJfKquFtHlnl7+KSuWwSJsdwiK1vybm1HgcdbpGkCpvhvsVQ0UZQ==", + "requires": { + "iterall": "^1.2.2" + } + }, + "graphql-config": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-2.0.1.tgz", + "integrity": "sha512-eb4FzlODifHE/Q+91QptAmkGw39wL5ToinJ2556UUsGt2drPc4tzifL+HSnHSaxiIbH8EUhc/Fa6+neinF04qA==", + "requires": { + "graphql-import": "^0.4.4", + "graphql-request": "^1.5.0", + "js-yaml": "^3.10.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.4" + } + }, + "graphql-import": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.4.5.tgz", + "integrity": "sha512-G/+I08Qp6/QGTb9qapknCm3yPHV0ZL7wbaalWFpxsfR8ZhZoTBe//LsbsCKlbALQpcMegchpJhpTSKiJjhaVqQ==", + "requires": { + "lodash": "^4.17.4" + } + }, + "graphql-language-service-interface": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-1.3.2.tgz", + "integrity": "sha512-sOxFV5sBSnYtKIFHtlmAHHVdhok7CRbvCPLcuHvL4Q1RSgKRsPpeHUDKU+yCbmlonOKn/RWEKaYWrUY0Sgv70A==", + "requires": { + "graphql-config": "2.0.1", + "graphql-language-service-parser": "^1.2.2", + "graphql-language-service-types": "^1.2.2", + "graphql-language-service-utils": "^1.2.2" + } + }, + "graphql-language-service-parser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.5.0.tgz", + "integrity": "sha512-DX3B6DfvKa28gJoywtnkkIUdZitWqKqBTrZ6CQV8V5wO3GzJalQKT0J+B56oDkS6MhjLt928Yu8fj63laNWfoA==", + "requires": { + "graphql-config": "2.2.1", + "graphql-language-service-types": "^1.5.0" + }, + "dependencies": { + "graphql-config": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-2.2.1.tgz", + "integrity": "sha512-U8+1IAhw9m6WkZRRcyj8ZarK96R6lQBQ0an4lp76Ps9FyhOXENC5YQOxOFGm5CxPrX2rD0g3Je4zG5xdNJjwzQ==", + "requires": { + "graphql-import": "^0.7.1", + "graphql-request": "^1.5.0", + "js-yaml": "^3.10.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.4" + } + }, + "graphql-import": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.7.1.tgz", + "integrity": "sha512-YpwpaPjRUVlw2SN3OPljpWbVRWAhMAyfSba5U47qGMOSsPLi2gYeJtngGpymjm9nk57RFWEpjqwh4+dpYuFAPw==", + "requires": { + "lodash": "^4.17.4", + "resolve-from": "^4.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + } + } + }, + "graphql-language-service-types": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.5.0.tgz", + "integrity": "sha512-THxB15oPC56zlNVSwv7JCahuSUbI9xnUHdftjOqZOz5588qjlPw/UHWQ8V/k0/XwZvH/TwCkmnBkIRmPVb1S5Q==", + "requires": { + "graphql-config": "2.2.1" + }, + "dependencies": { + "graphql-config": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-2.2.1.tgz", + "integrity": "sha512-U8+1IAhw9m6WkZRRcyj8ZarK96R6lQBQ0an4lp76Ps9FyhOXENC5YQOxOFGm5CxPrX2rD0g3Je4zG5xdNJjwzQ==", + "requires": { + "graphql-import": "^0.7.1", + "graphql-request": "^1.5.0", + "js-yaml": "^3.10.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.4" + } + }, + "graphql-import": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.7.1.tgz", + "integrity": "sha512-YpwpaPjRUVlw2SN3OPljpWbVRWAhMAyfSba5U47qGMOSsPLi2gYeJtngGpymjm9nk57RFWEpjqwh4+dpYuFAPw==", + "requires": { + "lodash": "^4.17.4", + "resolve-from": "^4.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + } + } + }, + "graphql-language-service-utils": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-1.2.2.tgz", + "integrity": "sha512-98hzn1Dg3sSAiB+TuvNwWAoBrzuHs8NylkTK26TFyBjozM5wBZttp+T08OvOt+9hCFYRa43yRPrWcrs78KH9Hw==", + "requires": { + "graphql-config": "2.0.1", + "graphql-language-service-types": "^1.2.2" + } + }, + "graphql-request": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-1.8.2.tgz", + "integrity": "sha512-dDX2M+VMsxXFCmUX0Vo0TopIZIX4ggzOtiCsThgtrKR4niiaagsGTDIHj3fsOMFETpa064vzovI+4YV4QnMbcg==", + "requires": { + "cross-fetch": "2.2.2" + } + }, + "handle-thing": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", + "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", + "dev": true + }, + "handlebars": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "dev": true, + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + } + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "dev": true, + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", + "dev": true + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true + }, + "html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "dev": true + }, + "html-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", + "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", + "dev": true + }, + "html-loader": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-0.5.5.tgz", + "integrity": "sha512-7hIW7YinOYUpo//kSYcPB6dCKoceKLmOwjEMmhIobHuWGDVl0Nwe4l68mdG/Ru0wcUxQjVMEoZpkalZ/SE7zog==", + "dev": true, + "requires": { + "es6-templates": "^0.2.3", + "fastparse": "^1.1.1", + "html-minifier": "^3.5.8", + "loader-utils": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "html-minifier": { + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.19.tgz", + "integrity": "sha512-Qr2JC9nsjK8oCrEmuB430ZIA8YWbF3D5LSjywD75FTuXmeqacwHgIM8wp3vHYzzPbklSjp53RdmDuzR4ub2HzA==", + "dev": true, + "requires": { + "camel-case": "3.0.x", + "clean-css": "4.1.x", + "commander": "2.16.x", + "he": "1.1.x", + "param-case": "2.1.x", + "relateurl": "0.2.x", + "uglify-js": "3.4.x" + } + }, + "html-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", + "dev": true, + "requires": { + "html-minifier": "^3.2.3", + "loader-utils": "^0.2.16", + "lodash": "^4.17.3", + "pretty-error": "^2.0.2", + "tapable": "^1.0.0", + "toposort": "^1.0.0", + "util.promisify": "1.0.0" + }, + "dependencies": { + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + } + } + }, + "htmlparser2": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", + "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", + "dev": true, + "requires": { + "domelementtype": "1", + "domhandler": "2.1", + "domutils": "1.1", + "readable-stream": "1.0" + }, + "dependencies": { + "domutils": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz", + "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "http-parser-js": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", + "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", + "dev": true + }, + "http-proxy": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", + "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "dev": true, + "requires": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "ignore-loader": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ignore-loader/-/ignore-loader-0.1.2.tgz", + "integrity": "sha1-2B8kA3bQuk8Nd4lyw60lh0EXpGM=", + "dev": true + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "dependencies": { + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "inquirer": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "dev": true, + "requires": { + "ansi-escapes": "^1.1.0", + "ansi-regex": "^2.0.0", + "chalk": "^1.0.0", + "cli-cursor": "^1.0.1", + "cli-width": "^2.0.0", + "figures": "^1.3.5", + "lodash": "^4.3.0", + "readline2": "^1.0.1", + "run-async": "^0.1.0", + "rx-lite": "^3.1.2", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "dev": true, + "requires": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + } + }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", + "dev": true + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "requires": { + "is-primitive": "^2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", + "dev": true + }, + "is-my-json-valid": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz", + "integrity": "sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q==", + "dev": true, + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-svg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", + "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", + "dev": true, + "requires": { + "html-comment-regex": "^1.1.0" + } + }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isbinaryfile": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", + "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", + "dev": true, + "requires": { + "buffer-alloc": "^1.2.0" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-api": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.6.tgz", + "integrity": "sha512-x0Eicp6KsShG1k1rMgBAi/1GgY7kFGEBwQpw3PXGEmu+rBcBNhqU8g2DgY9mlepAsLPzrzrbqSgCGANnki4POA==", + "dev": true, + "requires": { + "async": "^2.6.2", + "compare-versions": "^3.4.0", + "fileset": "^2.0.3", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-hook": "^2.0.7", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.4", + "js-yaml": "^3.13.1", + "make-dir": "^2.1.0", + "minimatch": "^3.0.4", + "once": "^1.4.0" + }, + "dependencies": { + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + } + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-instrumenter-loader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz", + "integrity": "sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w==", + "dev": true, + "requires": { + "convert-source-map": "^1.5.0", + "istanbul-lib-instrument": "^1.7.3", + "loader-utils": "^1.1.0", + "schema-utils": "^0.3.0" + }, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "schema-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", + "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", + "dev": true, + "requires": { + "ajv": "^5.0.0" + } + } + } + }, + "istanbul-lib-coverage": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", + "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", + "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", + "dev": true, + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", + "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", + "dev": true, + "requires": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.1", + "semver": "^5.3.0" + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "istanbul-reports": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", + "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "dev": true, + "requires": { + "handlebars": "^4.1.2" + } + }, + "iterall": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.2.2.tgz", + "integrity": "sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==" + }, + "jasmine-core": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", + "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "dev": true + }, + "jest-worker": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", + "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", + "dev": true, + "requires": { + "merge-stream": "^2.0.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "js-base64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", + "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "karma": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/karma/-/karma-4.3.0.tgz", + "integrity": "sha512-NSPViHOt+RW38oJklvYxQC4BSQsv737oQlr/r06pCM+slDOr4myuI1ivkRmp+3dVpJDfZt2DmaPJ2wkx+ZZuMQ==", + "dev": true, + "requires": { + "bluebird": "^3.3.0", + "body-parser": "^1.16.1", + "braces": "^3.0.2", + "chokidar": "^3.0.0", + "colors": "^1.1.0", + "connect": "^3.6.0", + "core-js": "^3.1.3", + "di": "^0.0.1", + "dom-serialize": "^2.2.0", + "flatted": "^2.0.0", + "glob": "^7.1.1", + "graceful-fs": "^4.1.2", + "http-proxy": "^1.13.0", + "isbinaryfile": "^3.0.0", + "lodash": "^4.17.14", + "log4js": "^4.0.0", + "mime": "^2.3.1", + "minimatch": "^3.0.2", + "optimist": "^0.6.1", + "qjobs": "^1.1.4", + "range-parser": "^1.2.0", + "rimraf": "^2.6.0", + "safe-buffer": "^5.0.1", + "socket.io": "2.1.1", + "source-map": "^0.6.1", + "tmp": "0.0.33", + "useragent": "2.3.0" + }, + "dependencies": { + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.2.1.tgz", + "integrity": "sha512-/j5PPkb5Feyps9e+jo07jUZGvkB5Aj953NrI4s8xSVScrAo/RHeILrtdb4uzR7N6aaFFxxJ+gt8mA8HfNpw76w==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.0", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.1.3" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.0.tgz", + "integrity": "sha512-+iXhW3LuDQsno8dOIrCIT/CBjeBWuP7PXe8w9shnj9Lebny/Gx1ZjVBYwexLz36Ri2jKuXMNpV6CYNh8lHHgrQ==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.1.3.tgz", + "integrity": "sha512-ZOsfTGkjO2kqeR5Mzr5RYDbTGYneSkdNKX2fOX2P5jF7vMrd/GNnIAUtDldeHHumHUCQ3V05YfWUdxMPAsRu9Q==", + "dev": true, + "requires": { + "picomatch": "^2.0.4" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "karma-chrome-launcher": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz", + "integrity": "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==", + "dev": true, + "requires": { + "which": "^1.2.1" + } + }, + "karma-cli": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/karma-cli/-/karma-cli-2.0.0.tgz", + "integrity": "sha512-1Kb28UILg1ZsfqQmeELbPzuEb5C6GZJfVIk0qOr8LNYQuYWmAaqP16WpbpKEjhejDrDYyYOwwJXSZO6u7q5Pvw==", + "dev": true, + "requires": { + "resolve": "^1.3.3" + } + }, + "karma-coverage-istanbul-reporter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.1.0.tgz", + "integrity": "sha512-UH0mXPJFJyK5uiK7EkwGtQ8f30lCBAfqRResnZ4pzLJ04SOp4SPlYkmwbbZ6iVJ6sQFVzlDUXlntBEsLRdgZpg==", + "dev": true, + "requires": { + "istanbul-api": "^2.1.6", + "minimatch": "^3.0.4" + } + }, + "karma-htmlfile-reporter": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/karma-htmlfile-reporter/-/karma-htmlfile-reporter-0.3.8.tgz", + "integrity": "sha512-Hd4c/vqPXYjdNYXeDJRMMq2DMMxPxqOR+TPeiLz2qbqO0qCCQMeXwFGhNDFr+GsvYhcOyn7maTbWusUFchS/4A==", + "dev": true, + "requires": { + "xmlbuilder": "^10.0.0" + } + }, + "karma-jasmine": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-2.0.1.tgz", + "integrity": "sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA==", + "dev": true, + "requires": { + "jasmine-core": "^3.3" + } + }, + "karma-jasmine-html-reporter": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.4.2.tgz", + "integrity": "sha512-7g0gPj8+9JepCNJR9WjDyQ2RkZ375jpdurYQyAYv8PorUCadepl8vrD6LmMqOGcM17cnrynBawQYZHaumgDjBw==", + "dev": true + }, + "karma-mocha-reporter": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz", + "integrity": "sha1-FRIAlejtgZGG5HoLAS8810GJVWA=", + "dev": true, + "requires": { + "chalk": "^2.1.0", + "log-symbols": "^2.1.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "karma-sourcemap-loader": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz", + "integrity": "sha1-kTIsd/jxPUb+0GKwQuEAnUxFBdg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2" + } + }, + "karma-webpack": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-4.0.2.tgz", + "integrity": "sha512-970/okAsdUOmiMOCY8sb17A2I8neS25Ad9uhyK3GHgmRSIFJbDcNEFE8dqqUhNe9OHiCC9k3DMrSmtd/0ymP1A==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "loader-utils": "^1.1.0", + "neo-async": "^2.6.1", + "schema-utils": "^1.0.0", + "source-map": "^0.7.3", + "webpack-dev-middleware": "^3.7.0" + }, + "dependencies": { + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "known-css-properties": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.3.0.tgz", + "integrity": "sha512-QMQcnKAiQccfQTqtBh/qwquGZ2XK/DXND1jrcN9M8gMMy99Gwla7GQjndVUsEqIaRyP6bsFRuhwRj5poafBGJQ==", + "dev": true + }, + "last-call-webpack-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz", + "integrity": "sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==", + "dev": true, + "requires": { + "lodash": "^4.17.5", + "webpack-sources": "^1.1.0" + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "linkify-it": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", + "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "requires": { + "uc.micro": "^1.0.1" + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "lodash.capitalize": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", + "integrity": "sha1-+CbJtOKoUR2E46yinbBeGk87cqk=", + "dev": true + }, + "lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "log4js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.5.1.tgz", + "integrity": "sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw==", + "dev": true, + "requires": { + "date-format": "^2.0.0", + "debug": "^4.1.1", + "flatted": "^2.0.0", + "rfdc": "^1.1.4", + "streamroller": "^1.0.6" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "loglevel": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.4.tgz", + "integrity": "sha512-p0b6mOGKcGa+7nnmKbpzR6qloPbrgLcnio++E+14Vo/XffOGwZtRpUhr8dTH/x2oCMmEoIU0Zwm3ZauhvYD17g==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "magic-string": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.4.tgz", + "integrity": "sha512-oycWO9nEVAP2RVPbIoDoA4Y7LFIJ3xRYov93gAyJhZkET1tNuB0u7uWkZS2LpBWTJUWnmau/To8ECWRC+jKNfw==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } + } + }, + "mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", + "dev": true + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "markdown-it": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", + "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", + "requires": { + "argparse": "^1.0.7", + "entities": "~1.1.1", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "marked": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", + "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==" + }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=" + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "dev": true + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + } + } + }, + "merge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", + "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "mersenne-twister": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz", + "integrity": "sha1-+RZhjuQ9cXnvz2Qb7EUx65Zwl4o=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "dev": true + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz", + "integrity": "sha512-MNpRGbNA52q6U92i0qbVpQNsgk7LExy41MdAlG84FeytfDOtRIf/mCHdEgG8rpTKOaNKiqUnZdlptF469hxqOw==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "normalize-url": "1.9.1", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "minipass": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.0.1.tgz", + "integrity": "sha512-2y5okJ4uBsjoD2vAbLKL9EUQPPkC0YMIp+2mZOXG3nBba++pdfJWRxx2Ewirc0pwAJYu4XtWg2EkVo1nRXuO/w==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz", + "integrity": "sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "mousetrap": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.3.tgz", + "integrity": "sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA==" + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "dev": true + }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "ngx-color-picker": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-8.2.0.tgz", + "integrity": "sha512-rzR+cByjNG9M/UskU5vNoH7cUc6oM8STTDFKOZmnlX4ALOuM1+61CBjsNTGETWfo9a/h5mbGX02oh5/iNAa7vA==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "requires": { + "lower-case": "^1.1.1" + } + }, + "node-fetch": { + "version": "2.1.2", + "resolved": "http://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", + "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=" + }, + "node-forge": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", + "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", + "dev": true + }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "dev": true, + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + } + } + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + } + } + }, + "node-releases": { + "version": "1.1.34", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.34.tgz", + "integrity": "sha512-fNn12JTEfniTuCqo0r9jXgl44+KxRH/huV7zM/KAGOKxDKrHr6EbT7SSs4B+DNxyBE2mks28AD+Jw6PkfY5uwA==", + "dev": true, + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "node-sass": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", + "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", + "dev": true, + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.11", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "^2.2.4", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "nan": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", + "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "resolve": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz", + "integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "nth-check": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", + "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "object-is": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", + "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", + "dev": true + }, + "object-keys": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "object.values": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", + "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "oidc-client": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/oidc-client/-/oidc-client-1.9.1.tgz", + "integrity": "sha512-AP1BwqASKIYrCBMu9dmNy3OTbhfaiBpy+5hZRbG1dmE2HqpQCp2JiJUNnNGTh2P+cnfVOrC79CGIluD1VMgMzQ==", + "requires": { + "base64-js": "^1.3.0", + "core-js": "^2.6.4", + "crypto-js": "^3.1.9-1", + "uuid": "^3.3.2" + }, + "dependencies": { + "core-js": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", + "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" + } + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + } + } + }, + "optimize-css-assets-webpack-plugin": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz", + "integrity": "sha512-q9fbvCRS6EYtUKKSwI87qm2IxlyJK5b4dygW1rKUBT6mMDhdG5e5bZT63v6tnJR9F9FB/H5a0HTmtw+laUBxKA==", + "dev": true, + "requires": { + "cssnano": "^4.1.10", + "last-call-webpack-plugin": "^3.0.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "dev": true, + "requires": { + "url-parse": "^1.4.3" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + }, + "dependencies": { + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + } + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-retry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", + "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "dev": true, + "requires": { + "retry": "^0.12.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pako": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "dev": true + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "dev": true, + "requires": { + "no-case": "^2.2.0" + } + }, + "parse-asn1": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", + "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "optional": true + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "dev": true, + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "dev": true, + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", + "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pikaday": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/pikaday/-/pikaday-1.8.0.tgz", + "integrity": "sha512-SgGxMYX0NHj9oQnMaSyAipr2gOrbB4Lfs/TJTb6H6hRHs39/5c5VZi73Q8hr53+vWjdn6HzkWcj8Vtl3c9ziaA==" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "pluralize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", + "dev": true + }, + "portfinder": { + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.24.tgz", + "integrity": "sha512-ekRl7zD2qxYndYflwiryJwMioBI7LI7rVXg3EnLK3sjkouT5eOuhS3gS255XxBksa30VG8UPZYZCdgfGOfkSUg==", + "dev": true, + "requires": { + "async": "^1.5.2", + "debug": "^2.2.0", + "mkdirp": "0.5.x" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-calc": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.1.tgz", + "integrity": "sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ==", + "dev": true, + "requires": { + "css-unit-converter": "^1.1.1", + "postcss": "^7.0.5", + "postcss-selector-parser": "^5.0.0-rc.4", + "postcss-value-parser": "^3.3.1" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-colormin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", + "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "color": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-convert-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", + "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-discard-comments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", + "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-duplicates": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", + "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-empty": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", + "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-overridden": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", + "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-merge-longhand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", + "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "dev": true, + "requires": { + "css-color-names": "0.0.4", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "stylehacks": "^4.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-merge-rules": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", + "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "cssnano-util-same-parent": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0", + "vendors": "^1.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-minify-font-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", + "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-gradients": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", + "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "is-color-stop": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-params": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", + "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "browserslist": "^4.0.0", + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "uniqs": "^2.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-selectors": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", + "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "postcss-modules-local-by-default": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", + "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", + "dev": true, + "requires": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.16", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.0" + } + }, + "postcss-modules-scope": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz", + "integrity": "sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + } + }, + "postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "requires": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "postcss-normalize-charset": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", + "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-normalize-display-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", + "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-positions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", + "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-repeat-style": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", + "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-string": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", + "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", + "dev": true, + "requires": { + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-timing-functions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", + "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-unicode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", + "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", + "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", + "dev": true, + "requires": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", + "dev": true + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-whitespace": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", + "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-ordered-values": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", + "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-reduce-initial": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", + "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", + "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-selector-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", + "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + }, + "dependencies": { + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + } + } + }, + "postcss-svgo": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", + "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", + "dev": true, + "requires": { + "is-svg": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "svgo": "^1.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-unique-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", + "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "postcss": "^7.0.0", + "uniqs": "^2.0.0" + } + }, + "postcss-value-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", + "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", + "dev": true + }, + "postinstall-build": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.1.tgz", + "integrity": "sha1-uRepB5smF42aJK9aXNjLSpkdEbk=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" + }, + "pretty-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", + "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", + "dev": true, + "requires": { + "renderkid": "^2.0.1", + "utila": "~0.4" + } + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "progressbar.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/progressbar.js/-/progressbar.js-1.0.1.tgz", + "integrity": "sha1-9/v8GVJA/guzL2972y5/9ADqcfk=", + "requires": { + "shifty": "^1.5.2" + } + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dev": true, + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", + "dev": true + }, + "randomatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.0.tgz", + "integrity": "sha512-KnGPVE0lo2WoXxIZ7cPR8YBpiol4gsSuOwDSg410oHh80ZMp5EiypNqL2K4Z77vJn6lB5rap7IkAmcUlalcnBQ==", + "requires": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "raw-loader": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-3.1.0.tgz", + "integrity": "sha512-lzUVMuJ06HF4rYveaz9Tv0WRlUMxJ0Y1hgSkkgg+50iEdaI0TthyEDe08KIHb0XsF6rn8WYTqPCaGTZg3sX+qA==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "schema-utils": "^2.0.1" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.4.1.tgz", + "integrity": "sha512-RqYLpkPZX5Oc3fw/kHHHyP56fg5Y+XBpIpV8nCg0znIALfq3OH+Ea9Hfeac9BAMwG5IICltiZ0vxFvJQONfA5w==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + } + } + }, + "react": { + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/react/-/react-16.10.2.tgz", + "integrity": "sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, + "react-dom": { + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.10.2.tgz", + "integrity": "sha512-kWGDcH3ItJK4+6Pl9DZB16BXYAZyrYQItU4OMy0jAkv5aNqc+mAKb4TpFtAteI6TJZu+9ZlNhaeNQSVQDHJzkw==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.16.2" + } + }, + "react-is": { + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz", + "integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "requires": { + "graceful-fs": "^4.1.2", + "minimatch": "^3.0.2", + "readable-stream": "^2.0.2", + "set-immediate-shim": "^1.0.1" + } + }, + "readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "mute-stream": "0.0.5" + } + }, + "recast": { + "version": "0.11.23", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz", + "integrity": "sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=", + "dev": true, + "requires": { + "ast-types": "0.9.6", + "esprima": "~3.1.0", + "private": "~0.1.5", + "source-map": "~0.5.0" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexp.prototype.flags": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz", + "integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2" + } + }, + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "dev": true, + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "renderkid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz", + "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=", + "dev": true, + "requires": { + "css-select": "^1.1.0", + "dom-converter": "~0.1", + "htmlparser2": "~3.3.0", + "strip-ansi": "^3.0.0", + "utila": "~0.3" + }, + "dependencies": { + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", + "dev": true + } + } + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + } + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "requires": { + "path-parse": "^1.0.5" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "dependencies": { + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + } + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true + }, + "rfdc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz", + "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==", + "dev": true + }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", + "dev": true + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", + "dev": true + }, + "rimraf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "dev": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "dev": true, + "requires": { + "once": "^1.3.0" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "rx-lite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", + "dev": true + }, + "rxjs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", + "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", + "requires": { + "tslib": "^1.9.0" + } + }, + "rxjs-tslint": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/rxjs-tslint/-/rxjs-tslint-0.1.7.tgz", + "integrity": "sha512-NnOfqutNfdT7VQnQm32JLYh2gDZjc0gdWZFtrxf/czNGkLKJ1nOO6jbKAFI09W0f9lCtv6P2ozxjbQH8TSPPFQ==", + "dev": true, + "requires": { + "chalk": "^2.4.0", + "optimist": "^0.6.1", + "tslint": "^5.9.1", + "tsutils": "^2.25.0", + "typescript": ">=2.8.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "dev": true, + "requires": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "^1.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.0" + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "dev": true, + "requires": { + "camelcase": "^3.0.0" + } + } + } + }, + "sass-lint": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sass-lint/-/sass-lint-1.13.1.tgz", + "integrity": "sha512-DSyah8/MyjzW2BWYmQWekYEKir44BpLqrCFsgs9iaWiVTcwZfwXHF586hh3D1n+/9ihUNMfd8iHAyb9KkGgs7Q==", + "dev": true, + "requires": { + "commander": "^2.8.1", + "eslint": "^2.7.0", + "front-matter": "2.1.2", + "fs-extra": "^3.0.1", + "glob": "^7.0.0", + "globule": "^1.0.0", + "gonzales-pe-sl": "^4.2.3", + "js-yaml": "^3.5.4", + "known-css-properties": "^0.3.0", + "lodash.capitalize": "^4.1.0", + "lodash.kebabcase": "^4.0.0", + "merge": "^1.2.0", + "path-is-absolute": "^1.0.0", + "util": "^0.10.3" + }, + "dependencies": { + "fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + } + } + }, + "sass-loader": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.0.tgz", + "integrity": "sha512-+qeMu563PN7rPdit2+n5uuYVR0SSVwm0JsOUsaJXzgYcClWSlmX0iHDnmeOobPkf5kUglVot3QS6SyLyaQoJ4w==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "loader-utils": "^1.2.3", + "neo-async": "^2.6.1", + "schema-utils": "^2.1.0", + "semver": "^6.3.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "schema-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.2.0.tgz", + "integrity": "sha512-5EwsCNhfFTZvUreQhx/4vVQpJ/lnCAkgoIHLhSpp4ZirE+4hzFvdJi0FMub6hxbFVBJYSpeVVmon+2e7uEGRrA==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dev": true, + "requires": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "selfsigned": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", + "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", + "dev": true, + "requires": { + "node-forge": "0.9.0" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "semver-dsl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", + "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", + "dev": true, + "requires": { + "semver": "^5.3.0" + } + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.0.tgz", + "integrity": "sha512-a/mxFfU00QT88umAJQsNWOnUKckhNCqOl028N48e7wFmo2/EHpTo9Wso+iJJCMrQnmFvcjto5RJdAHEvVhcyUQ==", + "dev": true + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "requires": { + "array-filter": "~0.0.0", + "array-map": "~0.0.0", + "array-reduce": "~0.0.0", + "jsonify": "~0.0.0" + } + }, + "shifty": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/shifty/-/shifty-1.5.4.tgz", + "integrity": "sha1-1DYvyRTdKA3fblIr5AiyEgMgg0Y=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + } + } + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "slugify": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.5.tgz", + "integrity": "sha512-5VCnH7aS13b0UqWOs7Ef3E5rkhFe8Od+cp7wybFv5mv/sYSRkucZlJX0bamAJky7b2TTtGvrJBWVdpdEicsSrA==" + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + } + }, + "socket.io": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz", + "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==", + "dev": true, + "requires": { + "debug": "~3.1.0", + "engine.io": "~3.2.0", + "has-binary2": "~1.0.2", + "socket.io-adapter": "~1.1.0", + "socket.io-client": "2.1.1", + "socket.io-parser": "~3.2.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "socket.io-adapter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", + "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=", + "dev": true + }, + "socket.io-client": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", + "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", + "dev": true, + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "engine.io-client": "~3.2.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.2.0", + "to-array": "0.1.4" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "socket.io-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", + "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", + "dev": true + } + } + }, + "sockjs": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", + "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "dev": true, + "requires": { + "faye-websocket": "^0.10.0", + "uuid": "^3.0.1" + } + }, + "sockjs-client": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.4.0.tgz", + "integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==", + "dev": true, + "requires": { + "debug": "^3.2.5", + "eventsource": "^1.0.7", + "faye-websocket": "~0.11.1", + "inherits": "^2.0.3", + "json3": "^3.3.2", + "url-parse": "^1.4.3" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "sourcemap-codec": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz", + "integrity": "sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==", + "dev": true + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", + "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==", + "dev": true + }, + "spdy": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.1.tgz", + "integrity": "sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-7.0.1.tgz", + "integrity": "sha512-FfndBvkXL9AHyGLNzU3r9AvYIBBZ7gm+m+kd0p8cT3/v4OliMAyipZAhLVEv1Zi/k4QFq9CstRGVd9pW/zcHFQ==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1", + "minipass": "^3.0.0" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "streamroller": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.6.tgz", + "integrity": "sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg==", + "dev": true, + "requires": { + "async": "^2.6.2", + "date-format": "^2.0.0", + "debug": "^3.2.6", + "fs-extra": "^7.0.1", + "lodash": "^4.17.14" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "strip-json-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", + "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", + "dev": true + }, + "style-loader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.0.0.tgz", + "integrity": "sha512-B0dOCFwv7/eY31a5PCieNwMgMhVGFe9w+rh7s/Bx8kfFkrth9zfTZquoYvdw8URgiqxObQKcpW51Ugz1HjfdZw==", + "dev": true, + "requires": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.0.1" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "schema-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", + "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "stylehacks": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", + "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "requires": { + "minimist": "^1.1.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "svgo": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.0.tgz", + "integrity": "sha512-MLfUA6O+qauLDbym+mMZgtXCGRfIxyQoeH6IKVcFslyODEe/ElJNwr0FohQ3xG4C6HK6bk3KYPPXwHVJk3V5NQ==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.33", + "csso": "^3.5.1", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + }, + "dependencies": { + "css-select": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz", + "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^2.1.2", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + } + } + }, + "table": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dev": true, + "requires": { + "ajv": "^4.7.0", + "ajv-keywords": "^1.0.0", + "chalk": "^1.1.1", + "lodash": "^4.0.0", + "slice-ansi": "0.0.4", + "string-width": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "tapable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz", + "integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==", + "dev": true + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.2", + "inherits": "2" + } + }, + "terser": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.3.4.tgz", + "integrity": "sha512-Kcrn3RiW8NtHBP0ssOAzwa2MsIRQ8lJWiBG/K7JgqPlomA3mtb2DEmp4/hrUA+Jujx+WZ02zqd7GYD+QRBB/2Q==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "commander": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", + "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.1.2.tgz", + "integrity": "sha512-MF/C4KABwqYOfRDi87f7gG07GP7Wj/kyiX938UxIGIO6l5mkh8XJL7xtS0hX/CRdVQaZI7ThGUPZbznrCjsGpg==", + "dev": true, + "requires": { + "cacache": "^13.0.0", + "find-cache-dir": "^3.0.0", + "jest-worker": "^24.9.0", + "schema-utils": "^2.4.1", + "serialize-javascript": "^2.1.0", + "source-map": "^0.6.1", + "terser": "^4.3.4", + "webpack-sources": "^1.4.3" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.4.1.tgz", + "integrity": "sha512-RqYLpkPZX5Oc3fw/kHHHyP56fg5Y+XBpIpV8nCg0znIALfq3OH+Ea9Hfeac9BAMwG5IICltiZ0vxFvJQONfA5w==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "thunky": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.3.tgz", + "integrity": "sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow==", + "dev": true + }, + "timers-browserify": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", + "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + } + } + }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "toposort": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", + "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", + "dev": true + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "tree-kill": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz", + "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "dev": true, + "requires": { + "glob": "^7.1.2" + } + }, + "ts-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.2.0.tgz", + "integrity": "sha512-Da8h3fD+HiZ9GvZJydqzk3mTC9nuOKYlJcpuk+Zv6Y1DPaMvBL+56GRzZFypx2cWrZFMsQr869+Ua2slGoLxvQ==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^4.0.0", + "semver": "^6.0.0" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "tsconfig-paths": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.5.0.tgz", + "integrity": "sha512-JYbN2zK2mxsv+bDVJCvSTxmdrD4R1qkG908SsqqD8TWjPNbSOtko1mnpQFFJo5Rbbc2/oJgDU9Cpkg/ZD7wNYg==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "deepmerge": "^2.0.1", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "tsconfig-paths-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-S/gOOPOkV8rIL4LurZ1vUdYCVgo15iX9ZMJ6wx6w2OgcpT/G4wMyHB6WM+xheSqGMrWKuxFul+aXpCju3wmj/g==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "tsconfig-paths": "^3.4.0" + } + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + }, + "tslint": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.0.tgz", + "integrity": "sha512-2vqIvkMHbnx8acMogAERQ/IuINOq6DFqgF8/VDvhEkBqQh/x6SP0Y+OHnKth9/ZcHQSroOZwUQSN18v8KKF0/g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, + "tslint-immutable": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/tslint-immutable/-/tslint-immutable-5.4.0.tgz", + "integrity": "sha512-8lZG7hNYRFOJv/p/Wb8/1cgizWSRpn4W3GSNWUVye9WyeO/LRbxp88pzNO8Een3RCMbHa3o7oW2UWa+Sx6hCBA==", + "dev": true, + "requires": { + "tsutils": "^2.28.0 || ^3.0.0" + } + }, + "tslint-webpack-plugin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslint-webpack-plugin/-/tslint-webpack-plugin-2.1.0.tgz", + "integrity": "sha512-subYgmwihOGftPZS59looqPWdbqMIvsoTy8MeQPeZ7bOdwZfR3AAnVG8/VzpSRly8l/xbPosrX2QKtJEZPt71A==", + "dev": true, + "requires": { + "chalk": "^2.1.0" + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typemoq": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", + "integrity": "sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "lodash": "^4.17.4", + "postinstall-build": "^5.0.1" + }, + "dependencies": { + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + } + } + }, + "typescript": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", + "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", + "dev": true + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, + "uglify-js": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.7.tgz", + "integrity": "sha512-J0M2i1mQA+ze3EdN9SBi751DNdAXmeFLfJrd/MDIkRc3G3Gbb9OPVSx7GIQvVwfWxQARcYV2DTxIkMyDAk3o9Q==", + "dev": true, + "requires": { + "commander": "~2.16.0", + "source-map": "~0.6.1" + } + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", + "dev": true + }, + "underscore": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", + "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "dev": true + }, + "uri-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", + "integrity": "sha1-+QuFhQf4HepNz7s8TD2/orVX+qo=", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0" + } + }, + "useragent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", + "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", + "dev": true, + "requires": { + "lru-cache": "4.1.x", + "tmp": "0.0.x" + } + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "v8-compile-cache": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", + "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "vendors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.3.tgz", + "integrity": "sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vm-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "dev": true, + "requires": { + "chokidar": "^2.0.2", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + } + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "webpack": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.0.tgz", + "integrity": "sha512-yNV98U4r7wX1VJAj5kyMsu36T8RPPQntcb5fJLOsMz/pt/WrKC0Vp1bAlqPLkA1LegSwQwf6P+kAbyhRKVQ72g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/wasm-edit": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "acorn": "^6.2.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.1", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.1", + "watchpack": "^1.6.0", + "webpack-sources": "^1.4.1" + }, + "dependencies": { + "acorn": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", + "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", + "dev": true + }, + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "cacache": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", + "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", + "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "serialize-javascript": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz", + "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==", + "dev": true + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "terser-webpack-plugin": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz", + "integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==", + "dev": true, + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^1.7.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "webpack-cli": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.9.tgz", + "integrity": "sha512-xwnSxWl8nZtBl/AFJCOn9pG7s5CYUYdZxmmukv+fAHLcBIHM36dImfpQg3WfShZXeArkWlf6QRw24Klcsv8a5A==", + "dev": true, + "requires": { + "chalk": "2.4.2", + "cross-spawn": "6.0.5", + "enhanced-resolve": "4.1.0", + "findup-sync": "3.0.0", + "global-modules": "2.0.0", + "import-local": "2.0.0", + "interpret": "1.2.0", + "loader-utils": "1.2.3", + "supports-color": "6.1.0", + "v8-compile-cache": "2.0.3", + "yargs": "13.2.4" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", + "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.0" + } + } + } + }, + "webpack-dev-middleware": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.0.tgz", + "integrity": "sha512-qvDesR1QZRIAZHOE3iQ4CXLZZSQ1lAUsSpnQmlB1PBfoN/xdRjmge3Dok0W4IdaVLJOGJy3sGI4sZHwjRU0PCA==", + "dev": true, + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.2", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "dev": true + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + } + } + } + }, + "webpack-dev-server": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.8.2.tgz", + "integrity": "sha512-0xxogS7n5jHDQWy0WST0q6Ykp7UGj4YvWh+HVN71JoE7BwPxMZrwgraBvmdEMbDVMBzF0u+mEzn8TQzBm5NYJQ==", + "dev": true, + "requires": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.2.1", + "http-proxy-middleware": "0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.4", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.24", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.7", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "0.3.19", + "sockjs-client": "1.4.0", + "spdy": "^4.0.1", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "12.0.5" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "webpack-dev-middleware": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz", + "integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==", + "dev": true, + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + } + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + } + }, + "webpack-sources": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", + "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "websocket-driver": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", + "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.4.0 <0.4.11", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "dev": true + }, + "whatwg-fetch": { + "version": "2.0.4", + "resolved": "http://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } + }, + "xhr2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz", + "integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8=" + }, + "xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "dev": true + }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.1.0.tgz", + "integrity": "sha512-1UhJbXfzHiPqkfXNHYhiz79qM/kZqjTE8yGlEjZa85Q+3+OwcV6NRkV7XOV1W2Eom2bzILeUn55pQYffjVOLAg==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + } + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + } + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true + }, + "zone.js": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.10.2.tgz", + "integrity": "sha512-UAYfiuvxLN4oyuqhJwd21Uxb4CNawrq6fPS/05Su5L4G+1TN+HVDJMUHNMobVQDFJRir2cLAODXwluaOKB7HFg==" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 000000000..3bf616acf --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,105 @@ +{ + "name": "squidex", + "version": "1.0.0", + "description": "Squidex Headless CMS", + "license": "MIT", + "repository": "https://github.com/SebastianStehle/Squidex", + "scripts": { + "start": "webpack-dev-server --config app-config/webpack.config.js --inline --port 3000 --hot", + "test": "karma start", + "test:coverage": "karma start karma.coverage.conf.js", + "test:clean": "rimraf _test-output", + "tslint": "tslint -c tslint.json -p tsconfig.json app/**/*.ts", + "build": "node --max_old_space_size=4096 node_modules/webpack/bin/webpack.js --config app-config/webpack.config.js --env.production", + "build:clean": "rimraf wwwroot/build" + }, + "dependencies": { + "@angular/animations": "8.2.9", + "@angular/cdk": "8.2.3", + "@angular/common": "8.2.9", + "@angular/core": "8.2.9", + "@angular/forms": "8.2.9", + "@angular/http": "7.2.15", + "@angular/platform-browser": "8.2.9", + "@angular/platform-browser-dynamic": "8.2.9", + "@angular/platform-server": "8.2.9", + "@angular/router": "8.2.9", + "angular2-chartjs": "0.5.1", + "babel-polyfill": "6.26.0", + "bootstrap": "4.3.1", + "core-js": "3.2.1", + "graphiql": "0.13.2", + "graphql": "14.4.2", + "marked": "0.7.0", + "mersenne-twister": "1.1.0", + "moment": "2.24.0", + "mousetrap": "1.6.3", + "ngx-color-picker": "8.2.0", + "oidc-client": "1.9.1", + "pikaday": "1.8.0", + "progressbar.js": "1.0.1", + "react": "16.10.2", + "react-dom": "16.10.2", + "rxjs": "6.5.3", + "slugify": "1.3.5", + "tslib": "1.10.0", + "zone.js": "0.10.2" + }, + "devDependencies": { + "@angular-devkit/build-optimizer": "0.803.8", + "@angular/compiler": "8.2.9", + "@angular/compiler-cli": "8.2.9", + "@ngtools/webpack": "8.3.8", + "@types/core-js": "2.5.2", + "@types/jasmine": "3.4.2", + "@types/marked": "0.6.5", + "@types/mersenne-twister": "1.1.2", + "@types/mousetrap": "1.6", + "@types/node": "12.7.11", + "@types/react": "16.9.5", + "@types/react-dom": "16.9.1", + "@types/sortablejs": "1.7.2", + "browserslist": "4.7.0", + "caniuse-lite": "1.0.30000998", + "circular-dependency-plugin": "5.2.0", + "codelyzer": "5.1.2", + "css-loader": "3.2.0", + "file-loader": "4.2.0", + "html-loader": "0.5.5", + "html-webpack-plugin": "3.2.0", + "ignore-loader": "0.1.2", + "istanbul-instrumenter-loader": "3.0.1", + "jasmine-core": "3.5.0", + "karma": "4.3.0", + "karma-chrome-launcher": "3.1.0", + "karma-cli": "2.0.0", + "karma-coverage-istanbul-reporter": "2.1.0", + "karma-htmlfile-reporter": "0.3.8", + "karma-jasmine": "2.0.1", + "karma-jasmine-html-reporter": "1.4.2", + "karma-mocha-reporter": "2.2.5", + "karma-sourcemap-loader": "0.3.7", + "karma-webpack": "4.0.2", + "mini-css-extract-plugin": "0.8.0", + "node-sass": "4.12.0", + "optimize-css-assets-webpack-plugin": "5.0.3", + "raw-loader": "3.1.0", + "rimraf": "3.0.0", + "rxjs-tslint": "0.1.7", + "sass-lint": "1.13.1", + "sass-loader": "8.0.0", + "style-loader": "1.0.0", + "terser-webpack-plugin": "2.1.2", + "ts-loader": "6.2.0", + "tsconfig-paths-webpack-plugin": "3.2.0", + "tslint": "5.20.0", + "tslint-immutable": "5.4.0", + "tslint-webpack-plugin": "2.1.0", + "typemoq": "2.1.0", + "typescript": "3.5.3", + "underscore": "1.9.1", + "webpack": "4.41.0", + "webpack-cli": "3.3.9", + "webpack-dev-server": "3.8.2" + } +} diff --git a/src/Squidex/tsconfig.json b/frontend/tsconfig.json similarity index 100% rename from src/Squidex/tsconfig.json rename to frontend/tsconfig.json diff --git a/src/Squidex/tslint.json b/frontend/tslint.json similarity index 100% rename from src/Squidex/tslint.json rename to frontend/tslint.json diff --git a/libs/Dockerfile b/libs/Dockerfile deleted file mode 100644 index 33fb70728..000000000 --- a/libs/Dockerfile +++ /dev/null @@ -1,58 +0,0 @@ -FROM microsoft/dotnet:2.2-sdk - -# Install runtime dependencies -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates bzip2 libfontconfig \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install official PhantomJS release -RUN set -x \ - && apt-get update \ - && apt-get install -y --no-install-recommends \ - && mkdir /srv/var \ - && mkdir /tmp/phantomjs \ - # Download Phantom JS - && curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar -xj --strip-components=1 -C /tmp/phantomjs \ - # Copy binaries only - && mv /tmp/phantomjs/bin/phantomjs /usr/local/bin \ - # Create symbol link - # Clean up - && apt-get autoremove -y \ - && apt-get clean all \ - && rm -rf /tmp/* /var/lib/apt/lists/* - -RUN phantomjs --version - -# Install Node -ENV NODE_VERSION 8.9.4 -ENV NODE_DOWNLOAD_SHA 21fb4690e349f82d708ae766def01d7fec1b085ce1f5ab30d9bda8ee126ca8fc -RUN curl -SL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz" --output nodejs.tar.gz \ - && echo "$NODE_DOWNLOAD_SHA nodejs.tar.gz" | sha256sum -c - \ - && tar -xzf "nodejs.tar.gz" -C /usr/local --strip-components=1 \ - && rm nodejs.tar.gz \ - && ln -s /usr/local/bin/node /usr/local/bin/nodejs - -# Install Google Chrome - -# See https://crbug.com/795759 -RUN apt-get update && apt-get install -yq libgconf-2-4 - -# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others) -RUN apt-get update && apt-get install -y wget --no-install-recommends \ - && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ - && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ - && apt-get update \ - && apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \ - --no-install-recommends \ - && rm -rf /var/lib/apt/lists/* \ - && apt-get autoremove -y \ - && rm -rf /src/*.deb - -# It's a good idea to use dumb-init to help prevent zombie chrome processes. -ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init - -RUN chmod +x /usr/local/bin/dumb-init - -# Install puppeteer so it's available in the container. -RUN npm i puppeteer \ No newline at end of file diff --git a/libs/docker-compose.yml b/libs/docker-compose.yml new file mode 100644 index 000000000..bf885957d --- /dev/null +++ b/libs/docker-compose.yml @@ -0,0 +1,29 @@ +version: '2.1' +services: + mongo: + image: mongo:latest + ports: + - "27018:27017" + networks: + - internal + restart: always + + squidex: + image: "squidex" + ports: + - "80:80" + environment: + - URLS__BASEURL=http://localhost + - EVENTSTORE__CONSUME=true + - EVENTSTORE__MONGODB__CONFIGURATION=mongodb://mongo + - STORE__MONGODB__CONFIGURATION=mongodb://mongo + - STORE__TYPE=MongoDB + depends_on: + - mongo + networks: + - internal + restart: unless-stopped + +networks: + internal: + driver: bridge \ No newline at end of file diff --git a/nuget.exe b/nuget.exe deleted file mode 100644 index ec1309c7aa63c186bfb7d50dcfefb5d569f538a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5010552 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 z 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%0DTD6WFZRc|8}UCg~ZHuPbJ<@8Am?)@6@fy$er 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

w-M$=(Lt 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

rq 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�JGWsT6 zvAzK=J(n7AbmA;Ns^eWMhc(ycGDB{2@6d^3YP6baw4QA>SD?3-mM*wkkZY*j-t4^3 zNoqS5p?Ve?+8X^O5XyFzWh){x=NUHv0xyeySFhchop*NBytAV=RE)lkyx$mu%<4jB zQ}XH=R-m^ARPb1f#s`6-yY^xPSySzG zx7xI#0F5B>Q@1}^dyRM3axsFetuNs?JnaDeM(XuCtBx5|<}Mdjdw^cGs@eg1N5^m} zu&^?|U$TlLRf21ctOB%SQ5=tG<$u@N%h04rtT0=Dv^ONA@~(__c6! zj=hdPJNj(B?vK*>>XaN1L&tYf>ANYD$gj)#`Vc|TeHf3vjyVrq&^~_p4Im6Mdka$0 zOMGYE@x1VnMsrWVrQe^3m)xT^_J%x_!Z>k~yuq*GYGr;cHP@+@zXDj9mzQcSrDA1% z`L|69txzWGF+6(E9_Ymp-oBb*rCYJ1Z^%nR9O1`7v}|TZ=G@yNu{$C2^lR~XdSRZT z<8_%WIFdcZ=y)`!~xUw%mxU%o<#0qb4kz%w5=fx3CHE(I`AEF)& zw6V(M!-D3XiR(>^I_TK;7By(^OXLlnf~)pkTkSoC{mR*j0%&h}srFW?QSIHgo~Yn3 zKl(_PB!h1eiEsJg_It)V4kH2lE_VIK8x_?dy^VqRS1`V0%bU|TgR_;|DNA!y>+k5@ z)ylt&Xh%<08RrXHk!v>{y2o&I>0`uN#+|C#xv~6O&$hPw)U^&VvN1CFrjTh5zU#d2 zIqwI~yHs8&xC{>q-)_^Jq!@PXwkfQXL6yj`aXcfvVD#^Lj5%MxkoF`$N8EJF`fG}6 zWABZX?!NpwE`n!2L(5V}ag?_XToHU*^{B`lI%&*+G%p%2_}--kqy?nte^qVYdFIq` z5m*d)g6{(1}z~C(sH-8QuG4)^z9-Qu} z)qSfp>cQno`tRC_XjByv5Sc+Jh@pU@7<^UM4?f;ciqq-_4QOKdbhx6G+UsN5709Q7$Sv?za2>XTdMre^JMe=?RjvFxl)7x^ZhG@v|~)>`|F7@Z)ASJ^-u(8zAwMp z@k9+{HhDXcSsU|x!EL@DoJ+h+e(J)VpiX|;LdRSJ+Rfj5QC5#@Xg|h|!bqCgPBBJP zV~oZJR}nG92+fSEO6HDCH;fb@%}jo^ zw2PYcs*gu2#Oh+z(&IEwsBKxNt9A%7qO(ZF3E9E~6I>&_fs75+N5+p%O8s?=lv<=1 z)0D?r{uxToXCY9#jl6bkKAY1D6OAOM16t#y8dRD_ z*q|D=`g10*Z1~i=CbBvn&HUXUX>c{MGWqE(3uUJ2`z8(oT0rlM3jRJ%+>D6pZ?T-J zwydzKPt|?4s=BhaqAXh5N_W_N32X-T88h({rq$9@DLywyhalOC)%!5j+p%}Qs%Ima zoqueoT=*%+xjk&i6LCKso0`H`k&@p34a$r;$BlGj#;)nhA#LjlNqdY&(@(FXhnn(I zqk)v>P*d)wZnXpFB1M@#VTFbN)9fvuMO9X0=rwxR&GJ`T!HZ77g3MsBCgz$DZ*$pwsTMYh-;mIx8=J?Pi(1iDbvDbQw{ z^SGc0QH43t;U7W@7w8|%tKqMz@Y_C`M{5>*jQJ0x!U%8G@n(-z)>?@EavkzrzJ-r87>_NT125?QyXUnHDVGM2| zX0~!Yt}~yc4J9|k(6dtJlN)1b%6wuu*!67#NO0zl+~{)v)$?~%9j)1{U-`eudLaZ-0W5|n!}Y0+nNy3&D!PvLN}RuRzSC_ zOym3vbh9H$_o@M$Aab^@UkYGk@uyJFp;O27_cg^Q1(qaxh85_sV{WduQ5J^-b6TP1 zdPXvFO|mmhGIZLQJl0Yk|3tay8CIaTAuT_2`j}{IE7}3HThFipJvIoZiVE%)t_38D z9=t^g6bivFtPZM4$v;r7K7nfkd9%wFIu~g;;AD>2V89LhO5kmXA-rFoY!CCZ_pH4q z_1T{K418cLp^iW{3G1u|y#}6A*qHw4_{>G-%0U%3kJz<6>;KFlvZAekLr!8i;<8x^ z|CBu|;Sk3V(?Hq37N`F^MuAae85#vtcXSfLYgn0-wpymcNy%!N%v28JiNQl47Rmw+ zCoPsNc*dx&03A+}pR&g*9huqs3x|_zwano;LwFvGGjzrnmRd(F^$1_J>ls#{x3M_y z%rVgxDB3~Bc?ujlYfOxF72{xwp+Ij58R-m>V?J?YVN!WxhQT7Y2K)(G7zqNC$0nfC z9p!amlyt|U4aVObQ`~wgPRo)#!wU2^p}2l@i%h2yX{ZlXNsU)dK|ID3M2+;Jf@rZ! zLDZc{DFsoAK_~<;&d)WT)=?)XOHD@DsBMSTTwmhMkO#`)htKW8M{7tE=3MxA2$Zpz zyEM!sq|rN+fH~~DPLWADEe0r{3)t)-a^8;ZsF$|GxOo{=;B5Kpb8*_OnX z9ISlA0!vS*K#25 zkqgGKW3XU{fX0pis(Yyi(=A1_s?a`RX!VSwd^;u`F!)=_A*h`ThU9UB>RI~?q$ZMs zVl{)-z#Dx_f?We!;$MHxrr3B|8?^bM^-TC<&xdyZDl%X!t=lqeKyb+%vv z!!ncn2@frDxP^$^FEAe_4n$I%9KZ$0@|Zlv8|NA6+uFT52%HI%`Q}+pL)o_md~5GA z;G4;sF>`ezSz;IcL3a8XwBt7VeM}y+6f+`|JzCMUNt*i0FD03^@biy z@|x432ZsN3jq1Q48xaHCp+iB*$#}r}^&bY;~(b23LT4*+Ls>6ua|gT5A*`zOEK_wC+E6gF>Z7tkXM= zjCHV_IK55@x$4zt5@DprrjEFYs1j`>+rMpETexj*``g)dipTEwKg)G;(H4Z;OD=wE zw!Z+ZoE(-r+I>Z4`quqbVx{sE8dw4O(W>0HR0kO1Orc=~c%5=(=^-B`W^3ySr^_HJ2ijL%YBBzqTF_Irs zQ9OWI5X1uL7)iOFKA)SS-TRWTqo_TJjemaAHgQxTA=jX~YVEEjBFITZCuCPIHU!rlpJU z*eNXvIy!AF7HYt#3SyD#cyd;$h>3kaT@ee z5&@z0sJ~h#gx#)3-R*kR&~q+9qlZv*J)2%)I9$s~nt;>AtM6yR38v)*vgUF~TN#48<6*>E_4aroZVew*tLtmQTpp*soQy&B5uW zEM4Blt!D=)(1)mJZrFIbjC#Xroa&ffKWtn;+_3S2VG<#scVXmFxQFqP+4}PT6_3k+ z9Yh~MLofQ1|Bj4^J%HOolkA882sZd3_+N#a`SWRGy8YPp1N^)`gKXY>On2tI)k_zE zd_%MUUjl^v{~8YK*0-R`4>w-nD>@D-?b)=Ckeq*rM{l?aCF32;IKVCg*;p3(ctR&{ z5RATYy3x1Sq(*+u;yMr9l-%ohaDJ-1Wnytuu6ZblApRTAFkjIjQ{GB)Gm)jCPmzl$ z{|`CCFY}Zszq4fYrJh3yHO3G8Cq=z~{tIBybcgG{_oI(l{Ib$x_yZ=zu2!ZCfuAz8B_RjqHa zfOhPcZTxg->WFY;zXSHVb#=;z8QL73sgyS%Ber}=t3++}QeN4_A9}q1l{5SnPpuN& zMU~BV(#HZ|Cd%>FE-4nMZ0!=~b=le_o33~lfN|Ke>1KHyk%Bm`IijMudCx@~P3n79 zMip!H-cuw;78^}=6m2PgtSugdZCs$wbpMq-qw zTc(w?iEqfts0pGdqbWF)kp^2RqnB|wauPp|nzUz|J==Lkd2d~%Q!qlMBV?4$vT}x- zd8%}F7b7}IkOLL*qIBeVj8H64orMU^)(Dmu;ZnC9pP;3*AMWtO(63RjnU z(D+6@RVto|snXq>AAqWiC`X2yW|~T(jyaJXZ05BhmY!w5vE&L!T5|sW+y$^mM@M+& zCKl%9mW`q2d%`<_=6k|BOD_<2BP-KR0GwlD1245JHTX8esCCb7-V?>;6epfzHw7qI ztN;$=?*i|YFE(DOw{^+Eq=!`{9PZkU>Ac~gFt&Fp`T>_oCNue@2|9vE1HUtV%P0CF zu(l?P$kBHAxC0f2G~C4|f#nn?zcfcxuKuDvWf7IXyqw`Ko>KXrkfc3pX$U}-ljChq zRV+~1MSavbcTcuucN&mYPh$ z(m9vK%lSV&bpfe5ZJmwDUrY*b@%sX0;p=J@ zk@og|o1Z67Vw9(=S14Trd=rzI6#$_JR>naO{M#sBiI1@|Fe~>hmN#UUsA5EqU{4Hf zU>Fp(H~brQGh1{T#0tZ>h__bV4T)f&yq`zv2}@x4|3~r@=5#txbMBmocv2RS^g+G4 zmX=ahk)2iL46n#jva^R|=K@P#0Axpwr==7N*>PGbRo5=Xp78w-l)Iq;)G#sAUgQ zO+_uA6P0Lh&F?0DUHq~?>EEv-4|nkM^sjg}MjkuoHJ3I`BbKA*ShHpqv~q$Jp`#tJ zg$Gv#oPOI0n_6aO|FpOv*$xZ#yKDIy37Lex6LK49Ahl$+W+v(zdB? zWV7O=X{B&=rc#rS*n4-fA#Nqsm(z`hKsvfn9^o1U%yeU>Z)al8ox-@`M@=q2MzYA| z)h3X)NgzLN4Bv^TvpGte6-RyVocsFL=q|bM5547=yXL-|oHOZ|+DEa<7O|ES9w&PN z3Tmul;_MK;#!9o;{I?rl-sZm@_@wg5jtXk5YHCS#$H$K9Z?z{{S>&c#c`jLTN?2;y z9lXZ)YQ;9!bKQWQ>p##$DD<;^r26>53H5OhE5&NW`WI|7y*JtWfYz%9wOhotJ@E}< z_f&S0?c_16yx72yix;sr1{4bBSi#)nr07j_Xy@dC1E2{7{g%E^%$#u~6tURLQ9$+- zQ(g2}_+#I=6dk#z@NJ0#HwIN1D?a))>1IgKTWhY@4RYT`>QfEG^49qoQD z(*ugf`bJt7TaqY$9WW5x%1+Oj^G}1GG>1lZb41%Bb4fh&GHtt5JW3BI-9gorH01UK z&wCO@;fz*pRaL)7>w-F45{2UKgrD*`8xm-kSn;@IXNj6$be48KIg(*wh^HR8p~x7z zTZi^_r6M75iBLcs=Jy+hIco<|Cp)>Sc)X+hRN%!vGW%+n?Tos(fe~&f`3O~|yMH%V z5w?ey>x<1%(cT?c8&9r=x|-V2w(x1{WGWU@qALjFokcj+9Ybh1!EhzewZ2<-iY{08 zzCZz`vq^2XHMI6G#-$fQgyh_b;HTL;!^KINH=A3 z9qN?M=cgtlYr_$vk`5fN;cAdqwt)WRe_xsNc5r8)%QAn#ZsvC(u#NHWdT>9omDu+0@Zb((lMd(Z)+Noo^&$!Fsh@NK~oM35%H(df??5T+Dr`5AQeLd$~%Wd z+4CTKDu-!aPL&a{GYQv^7R!jRyl}LY0=vA>{i9aTS@Ij>v;cJfd?7cgo{IQGlIKuNdK^{k%$7gvu9rUJWZ=IO?22U3j zo6?Jwl8fLgA_FN+2U_X(g+4QoQgIVW@3OdF!}eu_5)t;QA_f(|cyb2>>_YfZUP(4+5i>hxDv~RJ1(YF*U%c`;H+xR4h0&~8j zm2WG`ckm4{O}N@qY>5>hT`W+1Yu4S`wc0i+K3XnO!8u#sCAKTe#^?Z5sF-q&+eImD za*Q+agu=z`Y<7ClF6J_B8J93FzXyuk1o9xw;hjMGvoYkuDEIGDW;0P4nf=43*)4A& zj_=m%MHN9g{Jt>gY|vJ-oAx)|vltLLIu(a8WQ~4T!_CR_&`mkDW%7s&&MZ~iF|%%=DhaO8-za4c=>ri@bU{d>AYN5o8#tS2Z5^b z*B~(50m|A2J|~ZG?DqG3Y*xq^@jgS&@aa4?;(e?*cM4I1SO82OIo_lIf4Fp1~pHqp;k)Ae> z<_>}i?=xRLBbKXH25rJG{F(Xc8Ev&8&8~*qZ0vW#pnN>D`ayUpS?K0^D;ks!B0He6<2@b@aWg1QI7PS8E%68mon*L^VZX6(u3x! zXTc=pMLc4bJ_sKLWIXC9ZA^NHHt* z?e_#S637ZbENQ$D{h)AP>a{kR+*$7=tw3nXeFHM+dWa7FaV}7}}mgbWW{iWG*leO0usP2Xk zQyFNA*z^AZ{x1LFlkmOne$-CRL_E-;&4lO%FdB0li%f_HrI}Ol85gcH;?x-Bsd;l3$na`G)XQzj+fHg|IJ9bCyD@f1+;6;iH(%2iOg7PLtJyLglS_XM^0 z@=!SQkv;i+@E&e9(4Bzj%WjVcJJtE9olBnB^UP~>5wFoSva8XmPTXR&O7So*e;{Z0 z`#d#TRg<~1zo8O<(JDFK6?%#VsxDAzwCYD1ty;pdbftEUI2cYt)*SwXAZ9Pt1De8n z0Q6_`(5IeRnjxEydy7OvIsB=hz7A?GNwV}8#6Kj6rzX<2RwA60vV~9$37>G%1 zyv_I#s9gO1xwWo6(BnVG2OgUZ)xT~zYQ-~Z;$dZ2j@tB^`8TKj+4n@@JpZGl`_bL| zrmnRGo=(rc>%z^PcWmih246m9W&LYNL~``Af7pmJZw04rNY$1GpOBkC7aGMoC}AfKU5?R916(Xn8e$1Sp+! zYoi|{COu6>tMjT><6l^?o}Tb(5T#Q^cTd=rPZRw#VF!;QIq%z@06lqN9lSX*!%l#v z7Fn9v<#Ls-+OZT~XEjT>nkGyHT8);q?yuYIG1+bQnCyn5SyN4&c<(vr9Y zk8w&uBeD~h#FEJ$t|SV9$Y{EF{A6uwAxs}%01o?b$yQ^|*qsy}kL+|dbu^9q1jN|@ zgR0(WrJFic`x(O7w^+dN1cHv-GbUJ%g8kfr2^fBsU{0{_!?UwF)<_NhX zpz2k4DFM@0)ynXr%%x`)KwF%$3ebk2KA;-b)*D_$oJ`LDv@|)7A0$S!JgTR6RSjHCEN3StO=IJgMXyoT(cr&A5p@7E1 z=s|EX_cl{`X^(!TFAmL+&SM5niah!?et@jnK3$icc2{lNUA3+K0|s?Yb($u6NSKx# zSQ)$=C-$(uIy$CybVPx0YE49>y&=(CL?XZ=1ZcSUa@o6^n zwHuZDu2^9@yHn(zzEzh$)wexqFgCDB;im2CJObDhP2HiIX;5wX>@izAJw3DABrdHj zH%(Yu!nY+y-5kwjS5`d*qZW~y_sES~7RrcjKJ(o<+OzBAj9F=$rW3}}%t(U5CwQbF zodsd1i#ZAU8xrJ&TzE9HW@1&1eW<8OrKkm+OeE7mXz}b$aH5dF%SDTFyPPdV<(H90hWzJ z%Ks4LWlP+z;bwctOAq--w&x=hNnz?#Ax{ei~dzZ9-WbxlO1$ z{w@G`{IT6WE~ z7B-ezc1xx?d5U;xG7LP%O6C-0Cl-K#M>(F2Wyv%SJm#9V8lFdj8~>ewa2olX2~PfO zZLOthxneZ8Zh*FcxB*&!U9Aoq(gsqmjKmmke0E0Z`@k1j8T|neW%LXVWz?bEu+g3` zcX7?1dBYOB2>*fUL;@%;*C0R38{1v&sdr}z-bJk&`U?@N@h>U8AQmXCuLqN3uj+DM zgreow+^Bq)t1DlTNBKS{XZVjiRldVw{C|w`0#LqkygP0b3sf^erPa8))T-&25oClzZfJ1W#!aBUA!&r!K`>gG zBhK}*6pHF0P$fK~!bq3v%jG!LlZqq}{iV&>f^O;PQ==ka8qaU$yk$$eIqzhX&lib; ze7=l>d|pPJ)L8jFrNX5<$tyrO68=@3fu90qU!;6udE9V3hjq3$M^8wmM0sGkBszrvSbtclafqIt4J1EQ&1l^x~yc z0OgK$|2I=MipME{PlHT{-pjv1xTsX3ws%jAcJdD2U2QP=ZC;|uAS!IZUw)oZxINC9D^~(`nH@Xp^>qr3ALx<;U1pG18O|C6T7+0DJ~NY3UW#_rEZ1yfh8X zxH*aL#Hw}6(abK7mF=eJK#)dDkiCKDr7$XkQWM@q5n`6NR`lO|66hKQiuF+q&liSg zz(6Zy95m+02=%|+=x)K`G92`>n+C1o>+rfomsbk+pTMmq_gUPcWtR{z*Y~cR;Wv3| zuJ2^kwu8w?5DP$)D`&L(y{xh+7O0*B)igG@$e$`ntra&?ZjV^zimX^U%ewskmvdlT z4L_@+kUk0RwhCy1XpJTA;@TSQ`i)g$bJ{xS)%825^7>fvd+5?Kis*T=WXF55K(u9O zW4yyIR$TaH;FU?elv|I-WTRtp?MQ0eq9tNNdi*=~TxoWX32!GSQysq%+x3U6UsgR{ zi;IRdrjGxG%aoR36QkOjQZHTB(T>^w6A4vEf?si6k3j}}nxAp#_rt3j;l4*J>myUO z7vN6x7ap@60>^CEk~cZM*(}4|q{_spqd9z$@cF_557HFA1Vs94!6Aqm?y2Ni>kD4y z!*6Hz#ZPz4O~ZEIlW<}YpQX(I7W`#)9EqzxwaXtx&+Wm_)4>w&a`>`KRQKY(29h&O z7ycIzSq)Z~F6`YxjaV@JKAygYD+8t09uE);dpRNp@hD-sLJi;#Ua3g&|KY*jV1lw+ zm}i|GCX5s9SWx39GH7fkEB5eve8>)4tlFriI*0|p4wK{USWqnNu+zzy>@e+EV9kQ4 zZFV|IyCgH3M6esYgFq3h3lYaBG&dMK$KH!*l(M_0Qjp5-o>N5X<(8TN+TA19vl)%n zaqdNIw@9LyB}y_8mvrKTm`%dV=G8`LEWw9-C{W#I9HYh9#b%sWeQEO`98hiruXQZ? z!-gz<2Bu!YPf6kM?_}o|-Y}nh*->GX!`GecWNmELexH-J-&Qos^~_y(sBH>(Df*)Qc(Me1_-Aq zpzUZ#y7e0CnTq+LYH403RGse;@_o3`_wYPy?eF4Phmg5=w$&i%#e;hFbx0Wa7mwN? zgtxIGN>nFJJXp1_K(-elsqz-Y!qOD$J^BY&hkiuFrqRlf)YqGMgrB1Ts2)YQ=IAZ_ z2V8rl%y}!!WK!0zFcU}B(oL5;+|E<>>1h)DeZ`|#0NJNCx=GjW@!sujrq=r@hvrWR%O3sgxb zNCUroHqwCEa`;M(S)bGP+t)UhTy!aP?4Ana_6bVVh8Z6#Zf_U|W@~R4 z>-h?z1~-KiH%RGxM=P7C?B5~v@O01*tg1^8z^!XFCU7vTAjtQ^f(FozhEsg4pk;*whB~(RoTXy<~k9p zCNm+Mt?E2&@L_f;U8?72Hkz~7+K;~m|3-W5F&M#^ z`VYF1{}wbV=N>?xGY#8Q(@=&9WARu^#tN$f+~r(1AW=ZlUS_cm<>y52fl+_pCyk&# z*-$I;?^8|ot7zs%3Gp``G} z>BqKubJNSK=rtnY#yZk<3%%eAVuKxEFk8K0OPunxaar5Oc*P1zC(;6=bPQu|Z5yO~ z@rwB8bJNTji@W`YrRxwTC9?pJyn=ySj@H0z$GF_uKG}LNs#3qXwH<_OWgi#w*P5^h zhg`IL%@_0J>hcBgs)X*1UQl`&omu;TY1;m8Yi)kGweAAk#@@S=t#K={zFhnN1?mL- zcX@={5>WQ9`tLL6wUt(*KrXX(dvy|HEyG;^Ydb@$_Jps2U$Pnx-uw05=icY&eYW0X zLET#T>SNjB`f99j&iDG2bKhabaAU(3Ac)r#iqXn0QYP=g_-brHF4CbzPwg8x`a=h_c#ll?m=fsPJ6Nb`oHP=^#0^q7bwr=8$f0eRz6QW=o z*>rnTuSm;?9nuIX&H6|F|6yLNo6fKB1v!?mhsx~eP4kvF%YuANe4mC<}T!yEC` zDxVmCV*O2gGuUsno1eL4K({=BY;g1j80d&Hk> zj6Vwi<@}+`Fb;pV%zb_SY=v*iA4Ovaygd~-hz+GbU3Hf6v4Oaty3c!T?5d|K9rdvCEt?_4DK=9{qlb`MQh@Pd`gLr#h!$YWvgGe5t zwFBQltN=m$F~QE0bm;OMNr&BilP|d!PIAAaoZ*l1RPN7J?hiFw0+4$--px0P1uDDw z2IYEJXg0Bjz7QQ^C zbPS^bn^BL$)9@|!MvB$8H`4O-41wL;dq<^q5;G5r+)$M5{WH!QOWFHZXIvE!Ck2IV z^*EHs{&}9ChJy4b8%pCS+NRY{nfU(dp_aVCIgQAN-Xg+VL;MIgs30>x@90*3kb$D1J*Ica%_ zh*W>=BcM?gJ%>ny4^tG$f%2^VPJPw=^vZ@?#h|Ouk{ryNcySOc8Ib(5vs=Jr01{B) zlS6paW%aW0rA(t;geBjq#;c4}N~39jEqJPP16{6}yz{l=&1!n5(af}f|a zB#n8U$@)ZF2l1ie!f{%Hw5yw|p2Lc{5r9^kM9%Zs6Av;eoD8HtKU+!`^Zoo@<(HKyuh!ORoL~ih zUfC&o<*;HX=zBJoenQyWtb9HLi1I;J$aBO;5FcaDV|f<*8*9WUB1X(YIm1Wulo4}* z88L=S0E`$p9{&`p!H97@@s`sYW<%NR7@}5ILnj5GDdk7jFFf=7+}0H-d0sO)Ro{I9 zSZ*iJntR{b`hw+T7jtz$N=veny>};^_C)_sU#@?6A$g(?FAp7S;q~FCl{SaZ-!MLY zmSj-`!#K1X-!>MU#D|kq{{)Ujxo>esVlW)zn>H8f`(7=&KRVjVH->(s#4mT0dB%A+IW#k(~!s+K9Q$pfG<+1$<$%bH(tyD z%NgySoVD?a1*&FHX$DySebw-Ol1x)SPIZ)lCiJ3HSJq%#VwMV`eb6jVv^@ytS^??D z9+SZGCtEClugliUu!!}}F`QK@Gsg8zSDgeGXIp^*JD0BROnE< z?UU1a3t~OpQRzD79?fMQEF@@2KCb}IqpPcSt!{5Q4KMWA{!mT}xi*Be;~2lZh?J}q z5xa}^`WK2ZYJr{7ORaS!q|Bg~z947#WS-JX7psK2MM*3GdP&Y`H~U51m5Pc5sy3An zdP)9$Q9EDl(G;GYlbYR`0XEb*iSx>gqGKtxC9|Mft&x0sX^v!pd*^&F0i0&Xv1!6g zy`W4@Rce)Km2g>JiU|PARYxT`gGAD7F#&LivbCwPO8`v($Q|vj*5#`bc7RMbX3Agl z=|OCz(3lGl?>R0NAcyS9v^0b6R;cr>?(s z;AJQ7mbkNUvp&e1d~1b!3nG;H^IAHC-%Y^Q=EHACR0n^C-*5PN?TExNuY;Lx&CeV@ zQ(Qcr;a=?#t5%gw6&-R34ttDLPA~G~aNm+HT%3fiIh7yPyfnh^kxHrbv1V|z6u({CN+tbPg?R%k z@8DX~(jxSCP&)=*B$t7iOWo~xA$Lgw!a^Dm7%tms9_~P8x(_pc}+VMcW z8_j}DwinCeO%Bb8STEsL=Y{VuQQ_>RYS<&WGi1^82r4z|QpL!RRhD9^KUP^BLNVPc zXZQl1QcRbs2LGF!veOVR6q6k9SY^cml^v^$VsghSiwmQbV`Z}G_~dYVT8_aXqg&bQ z4PS%OVgNwFjYoDguqcmg6u>Ui?rC3gu%+*@zzl|h1^EF)DO=u#Eq!(+>}-!wy|pWK zYLU(NUQUXd_q|DOY+sH)TM;ChCLO0$5}kEA&KSfhiQ5#(afnqCmx)7^#v28uIs>-lKNBB zhYz3ks~S_^a)Q)#8`x4(-}`?`{p!ZlH=iJNU46Qg)R(Z*<-_Own#R<(m>_lCXtk8o z_y3<#zgemGB-i3NH4!Z_Lr>Beo=@dt2EGm;K`}sboubykRE!Sxl`$|e*}=X#1|}9b z*w@CuM4*G+!W#p97dWEW&o79+93||M&pj;pAXZ8)gryn8Lhn2*A@*A59(v<=q3}Ww z4$U*|WccjcX<0*O->4u;+OX-{E$A~Q2C;GO+W`kQS7bl1nbBJb>P@WpwMgr9>HP*~BfSz29>FzNn zP|eBh%Dz)$-1vC8)Yuf}PJ&nf-S{YXw7a=ZjWvh8Qe#~BDF1M4bqSgVwj+H{ETX{; zks?}KDf{W+uTl2P=|sh4`ml=Xl)YVz7hcEG(ChoAceJ>};DcDQIrJIJl;!UrUOSqG z_QnZqM7aZr9jE=U<)b#z?4{#QPWUB@`fS{}Xk#Sybw>?ax`R87ml;kM`;eJwO)vXP z@@xza*sJjLgx{UG1@xF=E#QD!&W@%*+4hc%p71*#JUSJ&7g6#!VpePIaI&9#9b&5S zD%etXhKPH`Sdf3TrAB>DHsxU7C-OQ5nQLJkU8#|gUzcL{O4>Cc3q-qaaC6evmVAQ* zQt9Jf$txs%oXbr91kk;ba=jz?6^~0Q&zhy}N8BrE7ln)=CVE1d`m%73fd#Sr{cNvV zWLvKjW&0%KfQl_#QL17#24kv=td92R+j&?ewas_Q9fU7cR%+hMR^EMq{A-n~F4!5+ zibM`iAb{JD%|exM71ef@!U~h<#Or#@;N%A0xjj8Z_FIzc`Ed1&Q%My4RY5r1ymVIgF=_EZSeuVX> zYRu9x6=T}Xv=Yq*oAu#pg_9a7z_h_Ksj+EA=P_1l+BQ0zVyvvxz7c(-(tzDk=SfU* zBgiw(=zU^AXv;Y>Zk=161wkK15QaVvJ$}lbS~2Y#y*5!9wda(KD;O8j?fNi+K^ZCM ze&IazAqyvO-3L`Yvs}48fne2mWz>RGYT?CH&~XKR%F^?7H{Hq)m9N6c7(5BGoY7H; zNas{LQlew3(;eLAYU*NHQy03$Uj3&I{uXXJY4S0yGd+a-mwKJu5WP+T5nJNC)?Ib; z5>p3J3ngGC!_{y)n7s1+V4B`(fvGuD#ud98gAXQlxvF>BawJ6`fi6>s(O<|5E1M{# zKFj0m0Xpcqv2fh>YnaeMEc0^YY#!M$4I0H5tsJM`YC?kUNJ|mt? zvjP}7XH2x?744VsvTw0~;U-u_A^ovAot+(>ZcOePA?OT0!4yYlXEp4XAdMO+xh<3y zA~(*enPC{pI_rT#j;CXn&AB^nRocc>%?H=9zQOqz#Citj$hl*3d_s|90YhxsIoiDM zGJCQ3zqCBEoAd+av193ZJlN>`;Cbv#ua@c7M1$HH*DiH*yRo-y3Zj5yLvQ(h7S6h` zbtAhXV%b<4VqChNZ{6SZ2E8%g{~>y&y8vGiX=t2(V?$#J1KJ;Zy8xOd9((&1(EPry zqf4EPP2FeXTHgkMrF|z3<62lyr7!Wj2YSZ)Z7&n67E^-BUBuvIC3}9CXQCvWN37Se z<5l0weT$K)Olb<14K#gU?@JkI63GlS-79DKJ3KYebd`*Q&CCK600U5tH_)V5V+NW; z{%B=b5}6M)wKosxXaG)?0@xeAQD-SDf@QDh26#hF%x>Hy%cwazfGL)~At@8?p0*Jw z^%jyJ5Iwgnb6lCJnR}QS>MYHBh{00M!qU_$3QGRIfox$O|vFH<_!-ywX5kBCU?st)-&3eOloF8xnc&# z`u^Mj;riQpF+=5fl49YGx}PKG5wc!wezwpC0y=HL2)@$>HxYMieq^L~Hq~i~oOP_9 zhdU}d`idq=g%(s_*3KAM0NOx{3`~?dnvC0**UdogC;t3VX7_^B#==#~mM*7f}SYua<8=?525>CBZo!5^%uQuoP6X&JCZY&~= zrl8Ivjii9iLvL9NoHNHUCgn?h7h`8T*C?DhE1QndV4>qnd-8Lzq{aVCOWEmV_N+eksmt* zXDLc=PC&8~tT%1Q?M!==s#k zqXsG+-^Z25gUt^7F!E^BtVxnX2aQ?cB3Rn%uwYrQLmxqGa%GjbijJT@Ev@V{#5r-< z-D;NF2QE7n()3YewSa|@;g`5*i8F@Mxd^GCj;W5g%(50;;U&45Q_oP1elCTf(~PnY z!%(YT#+GBOxbC9@n`aD&83xJZ2^6d zwk6w1y1SBJ5zR%8b|IJ-BN!Geny#_k*Of%=G#xBbdvpPoOBzl#OY}25q7pYG)LxoT z`|mL7nFf;m6a5uX5PQBNDtcAEwKG7FJZOZt$a;@f#y5!d+7lg+zk38m2Q~UyN}p=< zwUmy;H#G?UOUh}Mcjq-^k9bQ|k&al7ylTnI4CQ$7QFW0K6 zn_-+6fR;|qXgAZ%Zb5}&foge;X0zl){^6I1q&2M8?`Dbf3INkXl?ofMb~&WVYBDYy z&j)KNmV>=SSdj>wl>zOJVui#?`lt5*b?n}NK?2ffPSdd z{%OfRTjNUlo`b6?hwP6oOzBAcPUPpwzPMZtS93_Rbpkqrc!e|~$AMxkr3Y_3L}+_0 z&bW5_I94xhK%@A3t1}N1h&uB)4t2(qOYIc(Qy#-7h~4v1-V>p79_EUb7`xgBa zj#9H|zvCHhC1E-PHKIxIGjR)H5*hSOelKVE5uWOsTqj{V7Zz~j6khaA?P&SvUmL-@u4?-wXXk@(`5ahd>|#gg_i{?7m#i> z6riVetf*ywD>nJ0=p;Ut{F+1&T8TwRi)-Clve6?LTOI0FA9b%IhsALXsN3pL`|u34w0uT&n?v2^P}eorRR^kspQaq1UNVnZ zo$cW-P(R^sJSIBsz(4kcYAsd$6q1PR3BT!u_ey6xrsu1IZ1;&(m%m*X$K z{FEJB@>kjZlQ*r^^Ynu%PTn?V`XOIO^n^sits|N_Z(3;+@czkkz>@%2t16?3Ink&2 zVon5`n|k*L#d(``9qD$7{5y#MD9}!bkX30dWUa5#!pcI{C31$J;;DtKUsjdA-%ty{ zLRL9ml~ycJS(Rotj;gdy&Js}rTNNa|2}y2Y37aC;gos)~C)*a8M7z>swg?mNl2JE) z_7WasQK&m7d#k~XaHE*6c%3YGkjd!^F?9J3P8RL~o7PzUiJykGvOnqd=lT7IpVEcx z{8QWcX#+$nKd%hMsWD|Z8_oL+fIVp}w9c5yQF_kG?oaqqcJuMzxH~?=eWYdkXd_f4 zvvCV8tH2b=b8?2C<*6dMUPa@skytxL z$U^hqax{g1kF+Ibsjz~74I<|)VgYFm`vDa9NPqFt{4NraR}SYGm^`R>R zXbo=vhUIAOsTg13A~HuD*7eoqIg(23?Ouo1Xk&*?cckf?yU^N8t0KcMTG_k_ddlVm zSkP->u%~!{h9_C{9(Z)xt5Ol4F{ zul^&K31x1KE;xuo$?)e$DSN)dGd$51mtXepTHIj=$oBS^lm9AGcmTr$$@{#TJ}H-0 zBS)`T`3c@qS?T?aBmFK;T0x;drv0`OP8_%`>h zNGJ094!@q(CHLY4-Hrf`d=edTDMSET0N9$;0eXz35ytb`n+)C2Pw0^)0YQS5-nV!* zP{09Gct!W9yT<5}5cM=g?<2m+dqUc4L3>-5Xc8ccnMPWZP;33z}KA zXY?>7=D@LhdNz|%B&a@;NebC(NVwD`#pc4TeTNRMP@e=IDO}E@b=|j}-%SwcbiDdo zPG`<5<))5TMsCKdTT@8r<0*%iURdo7%GY^%PdM7 z+A7L@K`4@`D8i7}gFBUtq8{9ELs1WmJCPK>twAZi10H-P5nnH>s_I z`t0tXRQ5i5^%=g*YVV?L&G6CwYVAE|6p>20_-u4j*Y&Fuf-aSd;FgSst#FU-rBK+$*J^YvDB7BOts!ZeKK~w4 z^58w;li)0FA~u}1>@J8yA;}E@jXc#G0HU3dr#0jZ_wm%o(~U9!_P2Zpz{ryv&j3&? zZMKpDz{ryf0PQv{GEKMj1!!LKqIi;aw(Gaud-V5)vj{sL{R3ZBPF>KXpG0|A?vgor zUuxzwwKj3)PBjjG@XRraVadlznBg#Gj=F4qT}F-9>9J&unT=au3y9kSn~h!Rj_m1> zv@&ki$K}(M!DQsDji1-AQ8|s-=lUuK=HNfF9FJ^%Qx#{l@*s7nwl4N%-(mrScrBjg z@GI2b@XMx?vpRTI5U&F)o@>G8+0#7M<~3S}>e*lVAYoKLeoBA(KUP21C2|X!-X0Uv zYfnH;GH9sXbA8?uklC15d5h+RsQN zT8}IYB*m=9HSL1)@yhjg))>b z$uwUp1DDkzl*%vJNC+K`4zDXflZ2VT>T~#O<l{>s1Pn}u# zRjD1f#!vt>%CKB-&Rg+7W~a}j{sT`*s*Ex(>&p|cKI;z^Sr;(P`sDBi@%m=NQDi;K z=^u-n7UiJah4~>=c4ByuJE@(eilwiXMZxX&%*BlexCkPfsfH&I(vTmG$JP%5hRG`^Bh@$M}1j7T$H7NLA-#cQ)nF3 z!@CKz>xS2zWx90`ZvnXOEPzb+0o$?>Hj^blTJ8J3>9MTf$fKSMk0|V2 zoaooLSiq5$#)NoOA&$58fC7fMf^%fnm|(wAuoEnpfZ=Tlw(^)@zg4i$S}*~_+X>c6 zCX`t#FFUp)*`Cj!>K2gi#yfg(i->X^_&1#sg3HgQr;}Y3GIn{?dvqemtqU8nX?@NB z3(Tl29{eQWC}c|^Rt%fRR^XPwD}=qtCHrwesIGUMQhr@Z`FSLe#07DQ@61~e>*>_( zy)HnWnKWM}fvXUzp7*~KFPtJ3Fb!;_>TpUqJKsVelU<6SPEO?E)U0NaJXZFeyFOJ* zvttT!y|q3mSn@qMU2TlVdCiix*Vd=OlO6Ukp=CUj9e@K5!9)xePMBd@ucMM~2k)#l zGn=QiL2k(uvoUU+$>$Wj$y-s-JfMzsN3+9#$~O)vr|kJHa%ojC<)?hpLl!kw#aT|3 zj+WYK_(mH*A-JqdbW7dLMf0GP1u*q+>aTrDCpD@LL}nCyXmu?pk+GE7m-R=IQ2v(# ztUXIfaZD-p^p4P+QfNnYNE39lnnD=7f}kFMJ6cC8e^wE@R!(K0Q>i`S*a@%b?7WF< zV_!$n2&jpZvIsZ;ba%PKTl1X8CXt)e#O-5+CxAAI$n}~y#RHi&aqJPX1J?V&-Am6 z8bxl^bM)`X(2^Bj;qmD!%aZi-$Ot&2!w!0PF(Fz zJJS}-M2(pG@Y3V7Cp^MS5mOku1Sd zvMg9emOfmIy{tw?jmIWQ9n01q99BTGv!#Acb|LydJod-k1MMOr^(X&Fc-(-yu)+U( z_&<;T6`ntzr91dm_x5CztcpkNzWi_I`)9bHVYD&x7ydhdwg9^uf0cm#KUP|?01Um$@#gdti=lUaPH%9Ig%>HKm6ue1 zS7u7Ur`1l+x)xFfR7j1G-f(4@SaiQde!7dX%W|}22C?jbN2A$@Z8*TN02DT(3X(sS z?aq|V9{hwzvcsLS%JFz*djS3%JH<1+28wk~!(YkM;)C$X$DVr1?hxK$Eb$OUb7P5z zDP%F0*lIE%_(WrgZ-RfkvBZ|rbI{x7#m~kP)g59C?>-#H5-sf@KAa?K%d0 z&;UKjk$esuqp-0c(VB#z19~b8CuTy$+8i%s4p;Ja%q4 z^SOsAwO&h%0myX(&(e13%^|(+^-c{!WfYAhBJvT)zbo@q_@#4-sIsM}o+`#5Edz!m z)lWvh^plOnD`kHS(!R4X$SFDB@-ax2Ox+ly;m^h(-+&U0L4Jx*iI|y>LH-L!UiSNy z?WDNq9y`_S(?YohlTH)Bj~W9%h|8)!gKs(d2^gZqIwJCZlQfN;yE}%5g7|0!nH2uO zAenzB^Tn!d`YT62B>sx?wO*)v+z2;-MBg%!B4XO>40kv)1M?b}pIa;Mi5wpTG^dp@ zPZ2T6p5Yo@;_G;RnmgPO(@E8r@yL(p6<2ZP{>a?ye&itjtSGowo`gRrh)+~rELJ%h zAa3+m2JQ@3Hopl^P9jkFW5|k#ko#7qL+79h!7XdS1C+a#TclGx4q~O{{5hM8oKku_SZ4WD_lXnb za5IIJTuhT(oQz*C7nJF}EVyGncv5L~vdcZ;9c9xkEtO~ms&lyBOwQoe(GA$SG@9hg ztOhEw>Ty67?wHPkN&d1k!MPZmbH&QZo_2;(JpGv^&p`aJD-x5>rlAQ`zh9fy58(j9-`cRO;UlOUV08BjK70Oe#No`Wj` zb+2w^VDvbJ%5h4-+#1jq!9myYF}$ncpLmAPY)0$tQ0QrwxrOgB+U-KwZK5?~i5YOW z#`6o7oQt9Nbd9H8d)*pOj;G=yIg>{2P#SW6)`os5>J7o7PIm4&%lbM{*W=We#o!Nu{M7rslgphpo}>)pu=w3NSUSY=vt0Z>ZJ!yUr0-e z)&K37eEd7*QeZ2Jy`Vkp4w_GWtXsb;p^e5_zuQ@5p`$I}T_6Iqam3}Wnw(SQt{Q~5 zDR+BP7lK#-4ka0u>n-86+*REY-m1TZSIZ-NsY&$3-4fmwK0Ja)7K11olaTHht#qPB zY8_;`Z?S;k1G+l8!h-6~=7CudO5eJwFRHAhB|09K&@b0oV7K@;)rwEl^{VUp<&a|w zgj)yp0OPb)w?LRNnH*#EJ``}lbEi^lk4E`&i-+56@i1QM1+Dc$T_W`tJA>917++Vn zz__)3fw2lFy-~Z%7CVHWz=}<`(1ttCR@XP>*DPgpO88U!qN91Q_r4GB@ODZnI%*7z zo4+gJHw0Ta2KI{_cAUW+LDI#u2h`UJZLz!4ajpWVtDnWHV)e76_xjGMUO&%d$!ZAw zJX`*3D=6pzdXTBb`sYIs4AQ%mT15L&`V+3-*IN;7H5>meaee<$!X1gfb~Y)2>~9J$ zTcP83;>!~cGqd{e*$-O>7a?+4XN$ya1$-!?Q zIJw@sMVN-z=xEnEn};=fQ2Q%k|_yrID|rk{sL#dzun~0M z%TH(iEw#WEl>)y?&hTYCRp8$$3S0mRT#i@ZiZxDwUx{}RyXO_WhiAG1m(NlQ+^C?y zuNDGF1_l1@qQC`Efy?y@+~^on;FjFd3VbQaxdQ*-$<-IQAvXFO3*3Wb1@4#kXDKI* zqj=@5!tlzwz4QR(xuTW#HGnAZFX7NH(7X_}>N>vCO%q?{l|x>5hA&C($KS3XEEN3> za)z(vsiOamczz%lgIEBHUXC{oqF5BYje}&{6`T&W1*C@5u(5#jpjqC6*#eR-L3ii# zPAQ#vS-w_KRCsxwV#nUBWO@DyAoBcGoYo30z~Dj3wi&AwI+rKG+MO?&Bzn@h9RpV12Xdmqht zC(_H|HZE|{Z+2%JuM7`$w()+}?Wwu4CfHuy#hR&4_B@2rgS6SXl|UeE+CCOeDcLyHkH|&hnZ|L*7H$Bf>T$w zGohVZ+a7oE4*JN#w3Iat`7!zL==;wubJyzKu+IANI}5&x4_{^KGU2ZBAbWFb>mq)O z?}mmPFK5gCiuyStJubGix&NKsGp6w#4Y)ZP2881$K8||$#)9AL!xhi}&8=;&Fu!OF zQ;enl5>Psp`YVMh#!^=|c@uo1vD9tAJ>FRA@+InSDJ!mBFp(G;UT|xc$qMju{sm{(y49{)>hg;Y%I@ zp@v0;c;I-GHhDz}Kn)`;e6O4Hk^jU%qKyc4P9C|so3@_*q7lfXqXyZ+b_SSq@ad$5 zht-8QD3`sMdHbN!>b0bL+m^^C&r%LzOVs9pVqK*fieHQg$eY}2tUL&}5)YdsGisYL z&+Ts(oY7BV^ryAbR#JvD9iKJy`S`2|@yBQFI~$)}D=&+Dd{(7hH$H3lTdXhFQ`-`< zG(M|xOZ3RcXSWB!8qpnb7C)>svwE|z99_#E2)miE96cZo($%mV^t4>Lw4XPY>)^F3 zCF@6rJq&Vrz=a#a?r1ul$sG57<&it6JM4-S)ww`G$L^xzcRn4T^iYj_gII3JSYxR% z0?=Ov7uFO$xAsZldcwy=oR+LUX8A;ie4J{I4%yvm``j8!Q(G4Ap^q$#tG~=Vw=CYv z_ctB#!%Tm-xvhrcPsLO;wknG$>={cs|5j;9}%Enx(d*7oA( z`yReb#{bQaDT`D)7y-fX8>AEN!CLHadk9SaA+nt3o_I#rGDOi@en25@OMHbDwkY03&2jpr{@+n!&Tyit~O$oVvP0*i{sEJJLRRIf!|g8WnEAWRSyokXL_3fUB++^<~l; zd3~wy>eZKZjn9G?^<{e9KA}sXZM^!@SvrIAUayGHUJTHr{cg#D`1dbfdfInh_?LPQmr;YQv&pE z1h}JQ*;#^z%gyvGH#XIpV#{c-ct&Pd;_w-MtD_fk%L8ntjv5`Y=%ts>MRj{xmICu} zn4)!Pm^NFUwmWaGFH#XT_51blWcaC{UmuORmaDWr8UcBIRDt;Q(Y{q5cYqxmknkOvC9LRT3URP3^Geda?lt3OzbbI7@-5$ki==N~`IiBt!oN7EOYeBmnljZOgWm@Eb{3_ei zms??$HnsQEE=KN?>1hbtO&2RXx3lw{RhmVVSZJezrR&D#K;w}U=in-2jk}DA|ABJA zL4vyzBVQLK8d9OutD8#9p`tT|>eWH5ek3#aMvJIq(jDM><@(0Sz4N~!HpB{XoQ4TH z8H@NYyker|5M@z=K{k|aGMjMpypmVODXgGKxxUKuoAyQIA?7b-t(ORmGA*)cjA`*6uBqR#6B z2C;k!LqD0V06j^coLCu%bR23KzrIJ{KBDhy<(OJi4cbZ|`KbTm@90|uf^O4&^^h-(L@ zqtse48j_WWN^+uthWA?0krI{Cqb6zS0P7rn6(%E|MtLj(hYm2hEwk$QdgNJVIgzk~ z3x*2vnmd}q$1qt3S64)%rlZ*-WO_zOYH5nBykYx_%6Lpa^d1ULKfi6W5;S(Eio!UX zw{6O^-?rIz*0wFkWhHOhB#m`#o8iyewi_TzZJXpO(S!HZ^sa2@ZQIR+qW!uJXK@H1 zZ`;-_FFX*|St%p(C4b-G{|A;!hvE_T@rm$c<1N^Y>tZjM?OD{j^&uy%cN>bbqW`qH z@j~#4>fH}jCgbcIU%s>xb$*K?KAWOICL|soZC?~Cc z|6qSCyL|8|s$W{!a!->#H`X$ws0g+24?JoTE3d4k3A0zz+81`3BDf_+25+%7Hw-D$?1EaS~D7}LH2u6G@;rcMqz zxO=!ZNi}r?VpCI<3u@|{%14?hwg=(qmZ8tGnyO-DcC*L`u(ulx3$^)t84pp#5q^6G(wHDrze>JP- za70{3b4OEc4b`8GMSZUk?+t@m!MJQ#OSgvAAF-VJquk{J_%jtleZT5p7_6T!e+9x+ z{)|Pr{D}%*{_H!Gzg_dP%;itgQYU|gKa;Tgh#_+~ry z#DnlNzdp_z7g#`uR7SVp)sxZPj4r_^lF>)SoN;8dwR9H+_wgcr5N`{bLPn1Q42v(p z!Spf`*!V^e*AxFJZxT5H2DVk8SYFl=JOOkMV8`GnXh$A5Q=^ZT)6WBe^_}kdm~$me8(NGs5RDYq#7}4IU8MhIV)wq zob6lXyeC9p=d&+B81ix!$Lq@3@Mq=x5TvM_#k)ig-nY`bIK}%mdKWYD@_&pN*c(sa zEZz=4E(E-D4hJaSZ9Y%p%z}oZRMea0qmW|7C*3rixsvLz3 zq5_*+dq#QM(a%%oytdL#B)=DYofN&njLgs$e6p8~*Ov0}NY^s0)xn~_f4QfSDyD@V z)C|r&Je(?OskmjFZmo1-EA33OX&u>B6wpWG*Ez4NbP{@CA1hadVeDZy9PFLR;w!(c z_+8BJZ~UZ@?fex5t%hQ$({cj<12-h`7k@}%G1oRb!jfHxf=DZ-9lD~If$fOr@QlpH z;EzIfTy$@=eu3>J51R|Spj{_GwLzh{Ed^FSS<4Eo_%LJcHg{=^17#ztgCzCX^_>uXVbc;@=Wx|>)4B~ zj=dsz{5XKTj$votqN6#qK3|2PzjAaa;|%4IPY^J(sT66n@;dQr&oUs>w^+b%J+3jU zeO>V_Kjr9Vy>nKGaCU`TYxaZsQI6VZ)+`N859qHP9cF2Ka!eY3R~nDAT%m8VfFadm zPVHH~t@8ysSLe!6yKr(Im~cm%yQGlsw$iGWg3{O*PxJIoc18aoIMg7^I#MAn7Ag?5x)gO)B?Pq19Fw~!eaHv16q?^^BFYqn{zKi;E8oyugn+k9b ze%JAPf}hnZp{R!c&ScHz`^T$SpXw=P_38kkP_Goz>Xlvwq!=9@I~#4KLpHTG#MRJe zkWaVS(T+$-48!yW3|r6#mB9!E_a z-H@FbjX2&1$Kbj~l3qvlBa`*Q_?864byoxoPu7M*S#smC@4P}S8O<{r(`zNmPP0vf z;>ynY5?HeP=pu z|GXaMI!?8%PRB{Ua~=0Kr7rtIbuQ5(*KzLx!H#$zXYs)Ravir{xxCQqeKq2?NTGH( z%BTPIa`Y+O!7g&*Ufog!S@BvzB1OHhT#k+)kTdiP1V;)1@lFT+Y2IBRGx&=@N4Y>c z8+kTi^+)i}D0mUaIs!$Uqg@>7Q|U6T_|YHnBE@TnY<-F%@-Y_K#r;U>Z5Opvdx;9v zSB{Pqf(9&NfXlViWc19qAr}&8a3xXEh(i#IK7f8Jbw$rM#;+-=#`XQE^7Lap=fyfs z5oDmH5%leceZhr2Ucr{NV0!gKyhsR>z1zz+D%lnOl&saSc-Pocxi_2(p!PK${THTV zTRAVoAFG)gLe-aMclC!h7{z-}xoec^P}X0hgIIZl+6mAhnfk0qb26n@Bh55RCR4Iw zPX(9$TyP6B4u#Q)An-Eb%n?n6_nxcG7=1nE#vo&{*c|`$0~+DZV%c!!^|h~wQ?5Og zjMfNo0QGU{Y-{PFOQgM$6+_%wjpoz#ns_)pe~zc@vhMQk%waC7rt<_w}r_<~1Q_9=D7f zkA}+HGij^Q%CdXG4b~82_y}mHErp#lYJP(1jQnp2o-iiCjv_$;iv&H}C>w{(-E`mM z&9yhIBARs-y`pz^OM)eO&Ap$b_rIEe&8>Bl(&9z<+(Z#G?N_u@g(dBZwqRVhPt9@k+%7k3* z-4gwt$L!9gB~!uE;m)XLHcmb0r#qU`J&OxUeTTVygF$JywC?RC1*Q);U3nk&^FET< z$F_j8jZn@b+CoNbPd{+kv|Vn2r-Pf3o5E$Q%QDg*dugGfEBsFdnRO~!9?@0$76mRq zv>)G@{xMrU+rJvbi=nL?&X*Pxy(@{{Q+#?Ud$YAWIb$^j<&n}6(KjgDuJ9ZRZ17W- zjyxhxj%iug`)B&#i|N?eKC^Ok8<3H+`mv5)fi`Yi2xn1t-ssNIYKFnWowJm1{k>|G zUHjHz^ZTD}q(vQ!G{y6lf@x4g+2!bVC|h{Ji;!zOp`Fbl{z}39KkD8)KCa@5`(AdX zT}ieKwk*lE0GlFnvB8*bo6uWAODG8~p@#%WxS*wlyQ?Sx5;~znsG%kF-XWCGd+5!S z&_f9U0tCqNet&0fncbC@@IKG`$E%NaX6~FhGiT16In!r;W^s!sR^`%Ij~XFX>oOR9 zH#wdS{su-S)m%*Fg&ftoUcJ^aBw(-`W2kA`&!@48-QnW~C~9YVZ9mWUB>wY^md!F+ zM&jvgsXoVvffozuW02!A-%nHDjIT*qALlS#gX7`c(wE~|$&V;TmsiGLVvs$Kqy)jY zG2mr|@ERn_{4)l}5^0dr0XvBV=PF4bwl%ZKd`j2MvAKPSQ-5q9dggoZN9DqSC>Y1F zDNOvI#y=k&CtjXLH03`+Pk-zmUag4Bmtxj=p{z!VBfM5Rf= zm!ZuHvp9H+lc>CPsX?N7f*;d>MS?QZXS9?8m4KU+JXORok0?OAg)fqy+a7d^ppi^B z^3mRtc<}VP*N6{He=;4hoG6rDi;ro_V}y`I|2=(JY=cWMN{DB6=45fc>c!~-#-m7! zbV3rTVZ@O#(dmdZqBR9NXO2yKi*Eqsoz8+TFJ3mRyG(^K`uX$T#wBAzJRfS2r}Lqr z#viZPcQRhtD=m+7yrNRC8?RXW$(+n`5T)^o%C0ISm8;o6()MsWZ=WqEuoPw8-3*!+ zKMi01?uGj$q_8)sFYKi~T|rucyA8sAb&?o51HUw#;qFKn&QJFy{4(x8Q7?z&#J-Jb zN7=WUo!y&o9%=aZCY*1i3ab$QN`0utdlUYr`yeh*tXGj#{e2MJ+51PAr|3_yeWC0D zM^C;`6*$k?6NpJncFGW+^t zf4fA&P5W%%g;x<9qh*gV)K5t}^p#%;0Zo0e(YNx;^Q)*xAh@iVI=A zURLEwObP4l64rGUZzKT1Dy!B#ExB1lSVVOgQR(3V`6H*Bz`)tec!72e0ouav;Su(s z=R4bj2NBNMe-g0WE|hds<|ENuy#d2f+!lTpqtA`!bBv`vJssTj10&j{y? z>5W2ll+*CuAn%h4ys{SG3&Bw??2Pi(aA4>r{8BxQj*Z{M zaCYC!`E3kaF?qO^pvc27h&;Fx2Iw-$+wkLMSucP@-9qlaT|$M^-| zL0W>}8FSrvz*6p`f2U{Jl#@bmtmA#scCJMGdXG|#FDmb&@3|vo_DkdUy+Gi%CP?z- z?Vz*zFg~0~GV^}Ss*v+m7}ZD2_W?5>H1i?M;5F6B<49I@@_6j5PAY!t5d zt*LX{?$Tx@V*64DZvkFkEZb(qBA^wEWs&W2%BdAl`4keYJ34P!vb?Y_;#8n%b@3pr z!S8`kUFzyf0N>U3_3DdwQV33SfwR8%V|d4X@5gZxQQyqgoQ9(ZS-A;L-}}q0*KNa3 z=@6~`I2dSeV9A!BVAFEN$zR!_7nH0PL+37nk(=9xkq>r-4=?6}AInO5??2JUwE1Yu z^11bMtsA*+W}eacBAX}fZF8+VICmdwuZ)*-2Ya-oXLXpmu!qDst+G2|=*(N@^3fCI zp?{91I;RfwOw8>yc(3+NKS1GC1)}CZh2_>A-j;~z`qJt%0?~<9B(n}w1|oS`zh4iQ z2!{ex3_ZbHDcZAq=O=&V(3UhbdBvL;+N*yRyw|`S-MK>n;?ViX0;&OC`T@r+)T+hj zQnsdfKkK}UhX9R6kNjqGTrR$J7c+rppx`{(urjqqpueRE zn{1UKyq5a*yp;OpBYB6fbXD_u5ZP1*En|*l)H0~1h$7qR$9h$P1nXn`Zz@zo~I$t#{}a{MSLmrSk@zW zTX(uP{l!6_CuliOj2umHu~pxR84I zT-2U`eDn>tNc%0?7#_vFR!6BgJ7N0?Yfw3ePj!Pw*D!m+%^3ww9fpHFDF@lT;5aBW z?xlWToD%0ho>KklP*C<&i*Z{@()a@v-_ng`ix!yP-n6{ zbtbG~)Y<2Fb86U3xrb3_pBd26=ExU?BdRka5q0*3P#uk^vj;@u{_r&)2_SVQ+f!#o zWTQHh`Y;A&>O*%4ie=R=l|bZZ6P?=KTc-bi|C@(&&&AO-FnM2pyrZ%H{kvM=AzMf$G6NmNmL}fmF-HFY7q!3PJXS}6Ohn| zns`Vw-e77%0I3Pto|-Tshf@>Aw0bommQ`7(NNE$DO=`jrotiMdj?~K-f$ID>r5ZIM z+A=laC>8&j6oI6UR}w8NlB&2Q>NJQ%LuczO6F*SjK-b20wU29e_r|WNIMg?AnP{)J zD>^40?fKwgv0l$ab0!-=S9`u9+q*f|Sl{k$j%|1Tdbr$8dug#PABp2*JK~OwG1k{J zP37UT;v`0)9r5$jGm}!{hn1TlBjFp!%{)p#WY%?1jA6#8#tw=Hi90CJ;Om)BsBd4^ z>lxL@RL>-1KHY(=0y+(Sqx;8pw!6j()m_XEexk9(PUh~e?ucEC^yTg!dxVnX{xNw3 zyAe?1Kz~hDb8cPaDfpVU5MbZTWvq*Hy6Lv@K$vqom69(HGBg+kS6KXSsgA+G-7EL} z&OJfyadJoUaT@R2=}4~>3CH>F(BKswE(Q$0%m}(lU_Vv z>EX>V_cCVA#LU{h=5-=b%PiFLvMTIlvUQ@z#p@$1UIADqlI3-ngw?oCG*_$c>Gc_+ zCmycL84uSZWiP%>g1PEi)snY#SrND1_IM>*2T42I$vAy4f@t^3*ZdmC>5Ebhx^vId zG0Ig_+o<+y1pwKa(S+PMdRymP8LkL~G8DgAPB@JC@@1Zq=5!|j-hVW6Wz4GL;E*_` zD`vl|&MvOl*DK~|PU`D*)m`DQnnuR^o#tKzbEL&55lW|gHCdIFFx4r4Ld9ptaU%if zl*{rKpM*8S;*-l@t$38G?EMIyJ6L@t3sP*?)Kx$|t7)!8Ph%2On01h2R(0Vr)p1p3SIN7`#L_XIF_Ntt=y{M*GSKt;U(X>Auhh$!Sf9$+jPkK?ZfFhs(mC~C z7Y5oM*G4{Vzni0uZDFAH*TjQ8eE7^LpCG)od_4B|JjO)eK;?lJknaI`TJ8r0n-{jl zB=nv7&d)ry9KK&Mn@?+Nn6g!^fJqjLT7bDQOARaShi}`6Sgt;Ft`ri$*i+@ zR4bk&0c{7?KO|tV5!*4t2e`AQx)GmR-BALwRv=x@oZ<{01juQ0OR+teUmdY;D`>BU zWLI3L2#XP899xCDEgvZ3OgLK9b<|y^6gO5hJ=IN=pfbvNQu##kIQMsSo^8#`^3!Yi zseadCd`}o|`qz9Sd7OJ9FY6%Wj#3(|jMNDUsn){8JmiHxu(-KPmix#@ijFz^8*jNt_vHOvCCva*vd^@I{oMn9^z+x)tQ3vEvBo!6yEQh} z$i!fOjm*B2J$aX<@{+ERNh0gk$SnS3Pu{5zr8P3iR#is2E_OB$PsTR|7R&hNI6WD^ z+|m?$UNU~Zaz4yC^Z9eyFLXtQ&kdMFlbtia3^4qA1`F|45}tJP9v~jJ`eA~OGW}N{ z&H8By-R_CT;JSpX7xj~&C>C`AJyGA1Pf4s2b2lwU@-k6J=3)tIaT2td0{2>2@1(l1 z%3+zX-^yd?q=!5xrc@rz6<3glEdE3W&VdlgfOub(f%^w?i@&KnTu7)?|E|RYc>oK+X4HA7F11#g`5_<4_+x{B z1JWII5Vi%wUB8NMzLMyMeuz&RCu^Xa6Wg@)3iiCM9n1;WP0!7?GNROe%MtBq1v`Rc z6eaiP$WGQ*v;7jgk#sE2dx(lmXJ2Fd6MSCse1}SI7;?5-LKQmDZztoTpW_IZfETubq05~VXD1^!m$I{j`>@-CO@Ve5 z$7|V7Xk&}xO5q=f#2z=YD;F~m(^uyJ;#!SXw+QHNomu))cZj3K?Ho)xl`g{YTrdWA zCF-Q&sH|$mo5_0l%pSE70+u(2`rxe=8VWrUQ{4+c)gEPS)^4hZk-XfSF>QIxeHpoJ z;dzSxC&23CzeVx8lRpBQ=mflBGu_=dvparNI7RPOIAf$s$SU07?1dv9Dx4BeE8J}= zHtqtU0i$c0)9EDL!>+#9{QUHn=;h-nA^m*4oTU=iNy}MUCq(&MXJ}QOl>Ag>;NDYi$wpcy?Bo+4>LK6Huw%2>WomQ(RjlS1SfZMSRZ%qg6@DU+-iJ`fWpV0E0^U5 z#m6ChI67oz6|<@c;uV2kneU*Pzr+mJ)k^%hn5A9V+5D1XhNZujRjFW#rB92cD#d&x z04$YN>uyVSz6lGKjwdSZgOGnXKYh5-v$QhR7A}C>USf76Mh6nUf6f$OQxl>^dad|S zQULv(RRwCdVG2!Nq)ecFv{L4FA20nU|INELdpj=PcD=p(0<;Td%&_Vd6b%S=}M`~MUl%EjnkOtUo@m_wj z)n4pb7(7qMyX`LbZde#_YHYgO>pyAm1fg@5vc=w|5p4s1UZi!ep~LYXcuB4!a zogd@=lR>3&PXB~KrEyOG#6hKSPXDAq#eYtJ-=JbYr+>jg#eGiyLW7F=oc@Ie74JFy zliLRs?Kwj(J9YEX8}LvNu!2pE1s95SwApjTyOZ(v`8r_ja(w<_lYDfl;pw`GzF<|2 zlV+6vCcM*pyyXTqf2C!wLcsp*4w~LvNuEw2&dfqp`2AjZ?1&|$;iHk*`3 zhV1Mg<3-)+5g&-|nHJbtL>n)9{v4EcXz4jH@GT^L5rZV>rxQETP4Mu1YBTS{U>3qZ zVon>*FA1-EZ0?3!{qcASIUiU;H~;7^pyd88cgvs-6Boh@Q9;>VKo|2BpB|Sp;Na8r z_--KSICq()5C<-Kq_7uRC(z*eO}u|kyno1!@(CDhgz6l;pefk<3bv{R6EHZCmfeJ) z{wkq5S)^ZiDPar2Lnema-B*|49rxj`!0GkjZZlmi_`LdXUqH!leYp8^^fq$0d-%!Y z5|6+R_Dg;tHg{JoZJrn*iQr=htIEYNTh_n^Q*wLt2ItYJxGktx3r4p{NVAT%a*~(Z zf?ED3=$&oBWHDJ26SkFb&QAyyd+wQfZkBmIf`)J*SH-iPo|Tfe(=OpBq&)eDla~os zC(iJF;%5T*auLyMG_YZnlNL5Iw|qoKjrvY=cCu}$;tt&>d8b=MKNrVJ_2btu@Jc_w zpOnQI_meF0w4Wq0{C<*sC*#+@qY;x@3)A z$xXnL)&g)bHzv38y6(Jl-^^ZX6YeEFFSU+YYmi#eQXAD`RM2KC$#MB`1**dwF_4Ra z55)qa)Z2o4TQfjt_}06YuZuV0?u6clOgUs1G(pnh5&k0DO&gsyd4D->M7_UKW^ZUYwc_zs-VqjS28lY>VrO~L1-H@k^yIK8<{Zh*3Q zG{fhF3(;g_-$em46x##~^=TwTcM>9;gkTI_hbw8#B}uhnk%Y<@Ate1n0tU~}6Aqrm z=qz81FINXf&uF6a5`DDVV3YCfrTQwihnLAN&C;0$-dVkzFK#JNq32zq3zBHKItDjT zwpshfF}OB5sLzo?cm?4>yAo^Z+vkq4NNPQ+t8Xnm_96*cRXo~_pj+Jtx;0*w8QU^= zeN%Qy%Fb0p*gqs-InQ_Syb`+_&+6Uysf7?&c|If|J?P)Uq!AG`spz8Km> z_w~fkKO|uAf{Gz0?rlcJ;f>Z_5zrZKuCFl=&;o4inD7QA8o$%}ic7Tqf!6%kj_U2i zNCj1<8O|q7l9LU~zG$BF&zc@NlHk!K$vYjngX;P(_i6`TH~A~Yzi z@cSpfWUMd$pVM~f4w-YYeg6;mCR)4eIPp*B26h8cKfle(NRHb)qdsl(luy5&+jlZI z@N8OV(t561Sy#_3{$y@o9Jy9KSN*EW!2K7wRfuVwpGYXL&ffzpuJiZdv^w9CTZ=qC zXXy)0o&Rd<-nEsnV(%JDVl^EDvcZ_uhX6ZGvlmt1w(7l#px5x}V{IqTB)ZNlI@2rXIR?5EYTZ&Uh_{jCNHp2s$hu=Y4ST>CTPBwQ>m=lwc={Vgr8Yw%z+}d{{w=bq;naZss zt4?k$?WBF`gJQ|;1B9xIgSa1(TjiC??P7#t%s3Tm=&u0MK84=hQ8z~QQf_sUm5Y4~hGgRb`cxHSEzK;z8))3REt&&V3nQhgR%^lH4O6^)mni`f+g9+wL@ zVUocnZwDq>|3lvHW%{k@dK}m~%AOl=q;jMQsCFMr0Tw)fl5}3B-mSt%*p=Mb?qzt5 zQ6%Qzb2Q%>XScmIJ`&%;{mdNM5*e*w7gcVydIFpo!)H#HA*d7SD5d37b26r-oy@m$ zb5+LJHMU1qJ$_Ig^w2sekLQTVl}DNA-cWh$`#yJenLG|0Kz_@cG6TbrC4~&vfrj$( z7#;C>-G#s^hAT=NyheaR$fQ|u@O7Lm;cSH)9ymS$u0R-9MpN^J@xe?3xJR1{vr9L2e8S{EQfoUo;0o04<4;zBIWF-BK$y6Fb zlFm&HUTHd?HvkB`!W;E8xr9``;)GI}*^7|nMI^ApcsVqyjc!cVHFLh)rmjln1`gSm zIan#2fib_tC2cV9>1<|W-;nFPD+_xKy(!mkG?V+5T-&X|?C#rg-AS@T@5nV!!S#1s z(*0^DW|UV~+xD)!lC~>eqdbv|!t9-o#GiX?ZbaSkCge?6nuEO=d*~K@Bw>U*qkOCU z|LptQn~*QkT_@og<=X_m-G{3Uy+a<*mBcmR)@s8kYohaZ!{F2lbjGW;if2>N{+UlZ z=Uda@itBOWy_ME1z4T8bgg`-3pH}(oh>vYdkJpd&E}rmQY=UatHoQ|Lb}>FxKm0AV z9l=~OrK{5v+Ua$|ohStV{$l)&rrdQeavgolzn-j%#(;0vH!R%6mGFeQ@Yd;S2+ksf z@Gr)(o9g`T!Y{iY)8$l(UAJY5iRw#PTR3gId{nf!Qi_6eL}LPHp0~~5XMh&ZpC_=@ zSl{aa$ZNr`3Ws+*RTiW0O7Y3td+cB)Z}m3!Q0pbjg=_OUW-nJ5yq&Z2L(Y^9kUE@4GDn z)SBV_D0fAw@;_@0XWtgvQyT0>c!KA`#bw$xvn{u=Q7`&$CW?F%h;;2;n7ty9e6%Fr zChPH3`>lj>hceW`a&WGa!Z$8-yWP*_H{sJoj^2DUO_8le&(Hd#h45TUa=MayoJ)gr z8{LfZG2+ou_@#W_z~Q^_0V7@%LlXBuKAIu?C5eiwtwfzRZ3J!wG}8|oAcM;&V4xt| zU}SM@XDIn-Ch$Ud4=|@TW|U76EzA0})XS7zujKSzAv{TuQa1lD?eaAazx?gkK2f%G z-JE!>Np}hVG%l~fZP+FUY&Nj?b|)1`9J=?&4Jm^1UeXeSXK~(to*{d@kTG%`)?rI- zk^5sEP0o&WzWL24UnU-{=kv&?nf)??f7^!(U%wgUYX#fThrvG;fcR+kjPi{Nvq?i3 ztUTxIe&qx`t?wX2`6mE60}X`_|MIO|A1 zReUzDP8__mA>7@3T=*pgl-8N;3gNMQ=c7Fc<#dznNJ`>#GLLdKh4nK9viGI4`RqOM zi|1YTQcBre>)W(tZ0f)>n`^CqxAhh18*Z+(Gsgl??i~+5y*q_ko4b%7(*%!WF`;4v zM0=CW?5Xm}M`hgMII>j@aWP$?AWT>M!9$2cO(InIFX~8je+6YGMPX;z)JP<+@Kb=J z*)@WDwr>1#&Nky!uro>~o*J=c!6i5$NpPmZ$8oX~MbX(LB~v5HIx`;fOJ_V@5<5z^ z=0bJKz5A}4=FomQmq~?==Qu2~bpB2G^ye|`Tk{xODV~&%={$x?s%{>`;!ox==0b<& zG4>%;Rm{Y_pWI>s?g6>Q@N`b&bucoSiu+hjW)2`+q9@-W*w8QVNW;4N{hs_I-#?L4 z+F@NeZSC+{ar^goJSHz-TbR{$X1&Rf(wZ*Tqgw_wC|>oA;N-QtWFbxZK9Zq>g%`4v1Ju5NYYE`~Sn zXZZQ(NTSKsF*K>i1oUsQ5IiFxA01AR@JrQ`qi{*41(-EQfg*W@UjgjowIj>0%@(#> zUaN-+q1VEDTiPsMhiO&f#(W@(sS=7UodEwrxnKf3*+Y8HIa!^^qKNB+(UH~(rR&!T z`%dQe{*mTCtrOyKU7fJ_lR4>sLx}2xcvY2w`v|$knY13{wnc_!lnYoxM*~RZ9QkCN zle9(J%~ao)kK4*7fAFem!!~JPHdpS=*-(sF(N=Ve=Cgd76_>@A z^zDOt>E`C6a|o1=&czHKT8lWZD}?s}uoXfF4Ng`c-Fb&Kk_bE0*)m_X58d`Q=7&}W z=MfM4%_uhZn@xa->O~g6GcUkhbqP9;KgvUCQ>_j#>HAX}yjGkDet9+a*O%vCTdkUK zYQ;(Nz29h;hmoF-F2otWOh?_$C~F7cQ=C=KwyTLSwyRwtt8zZ3wyV8Dcy|MDBmmph zWO)aag+)|P6P32By*B8Xq{1Uy7ZD;IE1y>+5_$Pv}wrT*8VOmByvcPMrIk(5klG zKx_g`LXE~5+)Eaw<^KPUKcl>u>fq&M-q#zp|LlUo;Fb6%>qH7uD^8VMKMoW7hXf2> z)u>bb@KC2blYXz^-&7d9wh4b)hA*Ju2Y<6jk~pxF^lxawUpm7V@V}&gOB4RG8NPtS zrLX(KmMi9RADOQBGoesGw_|ZmNPVPh_-f1CZn)g)Be*>L;B5{`@$zCaHH&Y^6ikO+f9A%~?k+ayC z;g1o*@Ciyo>lRO9XR1VWvv~FwS(V!`b-U(kQYBk6Q^evXUYwegRqNh5+4(9gqI!y` z#&Nr*{5x%tz-o&?5E0$=L%FoH; zEj(uZv4Myynqpr(BSJtpkf`W5VXBSJvkx;B0A>ZV{zDA)@YOu*nBkJg17X)V^# z8ome`J3qLTWC55~ie%cDfgQ&aka5pv&g)386J%p;xC!XQ?bfDXGZoCq zynuBwzeuATwb#i}lgwY=6#Mjy2mu`tH#P-ZO2M4W3+QD2rlw#s6wJxIfORrIrIFUQ zI$E8~7kT@A7@1!jLGDd<+n01)!ivz8D0egOG{x(kc#zg$XCSnN$vIr@mH9}s^0Xyh zKSa7XJ(r)1-B^=&iK@h!#LHNke|iOb=v93rVbm{X-?#i<^Zjj2;_p;=(wxlJ*o&S zND}vq@_KN8=rI5(?X>AEi)ftk9l;*=Va!|V#Wx#o_wkm}rBsW5S=;+0!RV)JLV>k* zK3G6E^I0>WGxK@O-~?z4-zJmlQ@?|qHH3-;<@189%2Sx?Q@;zl=B{t`PXPMVvTEIX zBn_dki0WOUQlDD><@fj)9lnp*IqNcLY7ala-EX~UN11hb%im{Mdf>o^E41ttF4G5H zTk~Ek2hO$xg(Dx^@U;(j5a)GEHJv#$Ze1RcfD&Ka}dLtn&j+d0KN?Bt?=T=9^}|g&7>GLire~C5fM4XOgI- zki>UoRsM!4NqkR|$OsGx7XV3=RqNg}kwjq;)u%)yNtA#2Gd`x44=%D*uMfMW<$%~{Cw$7>t;kbm z_~^Cqa7VlbUm=d9GbQHMikp#0`M0ArAP_KEx5WLYoQmW=Pd>RR6qCb}9b%u#X>6+% zSN!_av9LXkdu(g1_+3SoXM`C1m|0HSMj9$aoA;1J`z+1JvTgHYCbYUb$^%Anhs~9T zKZ`$hScM$zDbhz#Zz3FQ*8BptfM+%3wz`_q?zV2WyRDmSSq*;!3h$_g#u^Pr!XbR( zI`n%sPmWNBM!PE!sVyYZ!Uifha zp{WjSf#0_LygDQyD+K>>ktcO$cdpdMKd9l&`+#K26pQKK8Re}gf}!_;`}O8)#n3l% zYVI@%YQN?I|3MJy%^ncM{Ue`36L)Ij$HkRqevDZjLkGOSHynq>m6hiI31;}F5}i$? zsw8u;vr3|PsU)Ats(gs4O7gxci8eduBLS!+vTEIQ`1|tRbh3m+RNo>hRTBA+Zksa- z;(`yr?VPKNo$}@RrZSuODPh~1)$`p1roijf^P6lrXwt{1?T?1>XUFfqh}xcYJCO|9 zeg1@1Lolu_T)mI8R+}i1)`4x$PNMw>)N$urB%&Mx$MW^xnU^Ogv$TEAp)zaoAu)yW zCKD3zIm<}XOL(~PMtza7_3eQ}4E_mP@R4u};C6M4>j+wK%vn=$Ys_W^!oK3qMWHK^ zj(qSj7;}GT=?lQcloi>v?tSWtq$6l0vZ>=bVu0my@B>07T128&D{hU{m5T)J9}+P5 zgy`c-ct)uQW<*%xV@Cxg!ddmJhqMQ6060s-WIUph(^}07{M4UF>JXncKeRIecIW5S z70Fs5XtyY6KYpZo@NfLmdN8Sc{W@w~CY3Lb?JHyZ+StA^w*L~_cgOav3*BuZx_e2Q*7@Y+mFTY-^Sl>GHa)J ze;t3{8p8+U@0;WAt>W)X}*}H ze*{66G2qn*e7_+7;BZGKZba=Bgj*;yLP+h@oEddL$Rk-us}-XTL4&_kZsi2R=>?Y(&n%zj|)(k|7J|AurU3JYN&?k27Qe zJ>-QOk@HQ+yJpA&ddNJC-B8elChVVN*aG_49gWy6P1rxpum$w7M>S%%Hev6UVGHPE zk8Z>+Hev6cVGHPEcQ#^|ny~lCum$w7$24NMHDUiO!xqrTp1%>hy$Sp08Mc5v_5zLA z9ZlGKX4nGy*kc>9M>S!G8Mc5v_P9pu(M{NUW!M7x*j?UnGt_gd;3|l}S zyGfmPHDT|cVGHPEH|f*yP1pxy*aG_4O$xQU3Huirwtzl%lSb`n!agv=7SP9TQmMU7 z*inWpppV_8QztZGS2An?eeB7N(lW6LyP9DO=wnZ5#GcfIJ(yt&=wmO^h~3wO{mTqn zKp%V2M(hQfu!k~i0e$Sn8nG8@!v0l;EufFRcq8`0P1wKAum$w7`x~(*H(?)?VGHPE zPi@4W(u93*hAp6vy+kASB2CzbWY_}w*g+%qqD|Pp$*=|Vv6pPbUaSfGw;8s8KK8Um z?8Td~56!Rz^s%ScV{4y}?T^ey>O=Z_QtVrMQnY8OBkfbCds5W%uG^Dhoz7&R)?tv) zk?uV?hA-xHkHZ=Yluk0o&^2?E*4=ZXh<~8QV7X=T0^0@=`)jqwd>kgr*xhMG?ZF!hZkd9*S+L~o$&3`0R?fFOm<}+k@kGBeo`HXg=(tO5q z+;FXzn3ly`>FYQJ7wYoG)?rMelwHC&!=0G}3HM|#h;G{s)9oc1${Q{1)s=+jXyMjk zHXBZ#dkjORd?Wz37R#!2FVC(Jn_Dqj3c=pmUcznL@~?mQMs&>9T#F+qKCwOCsCD%9 z?4CXrv>bu^?J|_9vmA~x5FX23k@{HW9mi@Rw8`p4mU{IOm#>xNgZML*q|~j@&GQCI z&>pq(J-?6Gdf8pS=QFN<4(;m7y~i&4pXz7u=@1V zvWKna%D|Y*F%Cb{?pb(*d*=m@jH~C=ExVu)}Q=$<+lrd4|x89F(S>K zK>!VTz4;BvULn{=F=y*QS$%EKX*-nuxY(DV(aIR%6PDpwmch*TDSQjz7=)gvOwaVq zTrsyf!{tt^i*pFi+~8>d^&s5Bqc!m!2$pBd z=@m$DuqE?I$*o0xYk+y6oU<$X3mw+yAvSs1uJOE!+QPnK8QRQTr#v z(PlF892u1cx`Rz$yKymfZa$ZQE?T;3E@JM% zkgsa_6S$u}vX@MhGo8&~< ziQ;Z`8TzlHC^3y?`bVr3@nFj{l4|uYOupxmKn|5-Rc^dhuekwU%$83P#4%p6*pjRF zoi=cyA<09gn?(M%T+=2V&7SooS8Z5P)xEA_;$m8f*H!x9Zn`q$K}H1&0%1;j7f9mo zhZMjGu;xRiE+_G=t((4o2YT%sJm`CAO)<9=(Blbv8~(~%@~1MKgu+;apVvl6M;3yG zEXmwUuK!iAWt4$k4oe;fh%bzrEp2yql;k+TY$r9xZA?%nDGr)uMBs?6@|kpex4S0+LrbAK6T9;W9KKt zHeOdCo@i+dPLi2v9?Rei3-BopVBf6`-(MT?7M^Z05iBdLxA3*|27NQf<(5V^daS)( z4j4g}$6`7BiQTJ7Aa`> z1s<{UOPs;g7}akna3nKSPmPx^SH)dk4FiRb8T^NG3fI7;=3`CltTQb_xpi$VS(UXh z_3TDX8g^%6m;iKOW!1VbX^j^Lg+)~Bn&tfKHXmE879GoI>k%rgi5{dSSRaVVjCNq; z#^m1rGJW$~q!fa0y8w-{Iw6+T?*NvpZeVt>Ay#!Gb9Yxa#x6$saH-^Ive zb#(3@q(0Hg?R!Ar+NMG)&fXvY@@BY4Q!txjSGT~x{e8KAfKh6T%&S)1lCSbs82M;x zGq=GEa--nmS5k2PN3ts0VT$wf5kBU;0B~NG$9Z89)edG2!+DFE5O;?x$NvMZ_QH+yLgIA7h3KSRFBESLW=^ zoZaT!)i8d78F=-jD|Z_8rO){OQ(#288y0-m@)KjSKVg-3$E&|o-9r`^4P#Y*?%aFI z9Xj`3a_@~%D-K|m%NXH4GWNyLUf%t%Gv+HjMt}RusvLl+y}WFbm_D}ynmFZ!7khbS z)w+L|baI46R0o>n{CTgp8u2Z9H#YWE_{vA}3r#~;7W7kht+BkOG$8Eg|B!Z)Ua z<+_6C__akQ0R6j)Y)=Y|ke1+A_>QCFqk75qI-TQ5-(nE~@nFwrvB4f)G6qbRs^yw3 zProKyDkC1GH8==Jw)T4$k^h$8hVUmDGx3S^utx&F-S~O!o_NaCgGIsG?-jIFL;JY4 z%$+gctYEhZj<;3zX@%fJ`XZ{6eav&bfxV!~#eSf`%>%HI3+-HCPrxtu8r&BnS%Ff- zwc?fP{T@P!{X+ugqXW&1FoPD=Lv4?cDAaF{P!fnjMOI~hOo>9PieN{Q%SQqr3bMTI z5yB!W+aAHTZf%e7pSZC|EjGra>ymD|x}HJWgY^LT`tO2k(t0RD3c>m=K;O)T?2fO6 zj3-q<@Z^_R@jDx0P-r! z8@>sPsBHK~UTyeRijLG4Z$ z1_GSylC_FzG+5$P)I%XNt*9QPH8@PQOnWr+r#{H>*tcV=ZesE8+|S~Alb=^lB?P?t zrWjQB#^!znd|{Kb!vP^@M`BU;#7A`8eps&T=jfDBDMC56guh(`9j6(CW|31Yh}==K zDo0>Sx`Pd?eEZa%w4fhoTOTh|Bay zY^U`lSVl{G^%&teS}4q6iv*xdW!1V*OtikyQV5PBD$Ul(Uqf!CNx@n*7;kj= z8`xsTtWOtq>Wgl&s#UtzNU*eNpX(;8GKib3f}&*K&2hLjZk`3l{C%D(-?j1El%H3o z;&M7KrGDwxgqv(-`a3|B>2X+;=^FT#qQg~!N0Yno7z~x*?^3R5&PO(TMLbAra0-yDzjNEesh(AS z3c;xcPWwB{<)ahu4@Z$n>E}M~z|w@nqQ=7$0l~vlvEbnez>&WI1i{##=3?ssK9B2i4Q@rGXP(KKeao@R_7p8cOAoX(*M8=%8HA zlvOziQ{^&R<#L>ei3FfrWO*7&SVYxJlqwf#DD`1P53j~RiM>a ze6P}P<*q!1cSGOoJMB9Qq&F$nxD-$FlKSQ&-od|&V&x_o>DWOAqtGbhAVVMNrSj)NT9P>jRZ{*`Z@+{` zdivR`w<^PQu0U=6a=;fenL8U0GItKvsA4ap=Hl%2i3Gu`5Kk#OS=5f8g@uo-{83ir z515jbG2)|bqWlFf&QpVU!pJ zW7~uLVqT{*gR}<)04S}b?a*4t^Dy*%evNGh`@L;a%acLLMj>c%arDiclv9|%Wbi^j z$l%3Ti43kMdV5!SA8YXV|004yM+C2uRk;XLBDjFqD0RWz=y)N5vOEzKR)Yv?n{{?c zm!=N9+8X4k8=X#*dEU(IUP4S9O*Zo~OkMFEE1njv9H8UZ>E}^_cEXgJj+)f!|>88wGzDPG|V}=q3Z*EYJsl z%9E&(?9X2$w-OniZt{N%K;-{6EaZRf70C?aiz+&rrvA^uR8t?SX{+AJCoTIesuN!# zXVp98(Nnz}3-z~&aC=vo4j*VDh0A$p%wP`5&ZD_k^3cKu!d>w|AIFMB{AJH#K};yN+(n~Pnx);h_n>lqzs&?Hn|QszX5LsSnvN?mLXeb z<8ZGfWbAM`Tv<6hbV}UZABH1q?k$T{mV1Td@(%vi>3|eCf8T44-a(rO0<>K_T zx59u`#)*nbug1edx%pd|AeD)(cm z9*vibsS@NP0jNi^ymm-f4ee0A@&KN3SLi{UUVV36A*~7ZcFwhGUW#soFjl)|xsSo? zaoXY^Z)y~kxF=>D2_?#K3wCi-A64XB32temRg&YPcyLiQW-v?D>MSa%%`%>?8oMT` z+j$eqF^$<@72IW+#;jXeK2KpH0o0huu5~jUaYukG&n>|ph)lDL@>k_cb3L{-c@X3~ zMB{SrsO0|Tuh4qrUCUKT%VZ@JcW7*W!w55iXykUH_NKFB8KgZv1nl=`&W2ZNM;hCf z8}Pe_pC?Bu;Ph;;-dWLjd4|c+$L{*tW9i3#?2CMn-5IU2lqna|-3W|$-zagvg3a0WsLHQm~ypaIpTb9>R z5LQD+fhk=jY(1LbJi0ah47a6X(KB6Rt$;=sY9BI4dpzzAZ{RO7;7U^3z%$ zcoV<0Pqt`*^+7V}=3ge}CelVMZ@hY&VDRc)EO@1PK)O;dDDm;tzvD?a>jPPlKH}*X zIg{y@yCgRo5q0$+_`=rru!42*h~4?^>_ zc`O!I1B*5FVl+o0d5-16=yu&Q8wuY-)|ACHu?BCe6M9qjHpMTXDSIOgW&cm&i)lmI zPZDjH`Y)5BV$mMGR(DV8lBSB5y7);91q)+hhRPNRNhu$60L6( z$K#Q>WGaf+^(*o&0X5h>y{x%E=HYR5r!{u9dBM=8pMMgixLPih9 zzZ5+{(G5&_lT9BFF2EAy1?ai{fmwWW1eb96=yS{?=_kz>Fe2`-YrjU9+U(}9eDrS% z@&#tFr3ACY(vVu3HtcL&Q|Tg7U&^Zd3sWskp9+6xXvjwb(9+2A9y}EmQE?p5X1Mj> zX)?)WlhziiMMpDp2BFeM!GpB8w=t8te#--CT^AkcyO#-n=WtKhR{;9Lk8)~DxQ9bJ z;6He!I)J-qbzcMbLAK^*L;p(fR_pyot_;1o;E!;ydW5;VtA}G3BYnB`{sqZ2dsE~Q zTpnB-CkuRae4oXa@N#I>uHGx^vaMKHOY4T>0Cx4ts&${0WLsE7b(ONsu3q`~t})RNykr*@x34CQxP48Utd`>}*|zEg;)#=m z+lwjL$rg(Ma9dWb`|P@8^TD-B7H-R5t!TGm@$0h*l8eWBT;L ztU*4Kqx?C$h3k6XMwgWUz3&@IC9PNQ=;eZYa?85$%b46dpQUwEc`O8Xy1;$BnY1H3Ufp=SC?I&;kCpOxaXyCQ z@l^cT8EK}%gcpg&e<51&_%7^>$BGvoFDa|C7^ZlHjw4P?0ky{x)K~e4_GM@~K3-qN!ZByOP;MMV zt!*-}G(m>f&&vVfD36&lF-OwRiUs|=ysXL$OzG!9C4MnEV4Epk=x14;eil}fex6|n zUa}+TXG^xNx&raU$s%Y=D%lk*76A}6S)P8jWSjN#vWmse?@0RDQbs?om?e#V=E{oc zb8n;^IpYOD`dPN8pDpcr{T%jDlh~|Fz*s*k^QNEeGUxL7(kNP^anzN!;k#<3!+9%$ z<|BQW(w3uHX&bRtER9OrgQQB^*V!72yu#0uL6xqjvlqv)e3mgF@wH~-y9s6Bx&O8o+>-F8KkqDN4{ZEc}K za|DXW+1hWWD9J}kIlN4Y?t4r+TQj{W4%bX;-b^{x=Bd?WS60RxhZszk80=sYD1gL3 zcCGu-vGK&E@L1B@jL6hX%b#K81W+7J`A9jPCrOvjgCzA?Mj+{7a-NUA5lM%DGIe74 zU2czoKy5enrscU=TElCu3M<;(((XcO|0?Mp)wO)elWAA6J-mQS#LZtZRTqSz~COk>E4+??gtm9Hw!#{uE{f{MO><$+ilx z5S*cy)z@DrHyZeNOtx1CG`yZ%$4Y8VGuOf#Nlz*k^yIp-Dr;a$Pc9{KIUmaMkpSpP zS)QI0R+FCOIzbS;WJl1GmTX&fJ>rRzMNclRWSzDZ06i(o)038Lvz}aAvG@s(q$e$9 zD#-d-(o~QcO8RQbfYVw4R6%5WdeYJ!R!=JVSWhbR!|BQL4$_cTF5|0?Tz!~^l%rWg z8ktrgjT+K}#2V7qk55nt+PmP%kV-YxkE{cKo9}6hvP5hS@{Hei5h1YqKL!oIy$!+*JuX_$2!yH30f<78|tvGH+djC%V>mL#@ z_;Z0fhL3Fu)}>$%(ckDF5-?bXf*U-pDOk6HJ#4`Q4A>z(een3EU_A=IjEeEEqk$v(2vd zYYT5h{dbna2;adIo06HJWE=|wbSyZjDcD2>d)_!DU~m_l;-baYAKImg_6A9LA?`v9 z^YY2J2PJIJyM<3Xf73Oy!`2fvrCiFBiM69xsGd%g#bP8YXd|R~#HDs}6D^Z6k_B`m zpVAbpPr)8H1_~Ig0Pl*m;uOQiJ6KVSKeY*OK?Qiy@B|E265~JS1pcVf?C&FCF)M%H zgt$;fmw=A0)0%=UtYD6n0vao)1#8(0TIlQ+nv$8t+>)fUg=BZ~jIc@xb`*P7)0Vel z-rkyZ$Lxx{(E16rV}V@W#z5fq%^aP(lW;#UeZ4W&gTBaju^8K|FY5Er_xR%M3}&#K zn$V}ntA;O6V`sw`p)!2gOjczRObuUFh8J^pC7OIB0NPSkt^3+!_#!N#dWNVpe35_n z6`gQ@qQzpiD4A&4X$nnSGSQMjlI27@FQ%62pkvdqoQUz(3SEA;wES)ki2QyZE6eW> z_{v9HVvdwwp_1RNWL37nRDRj?ljK(b@+-^Budo{PTWiR##cWYF<~M_klwT2(<~JGd z?3$0Zl|RRru=3G1n8EH8r?uImavxAgTNzJ&2n3#NhXqfxF}&PK9n%>%#lX7g_E=R# zgL?XQ6} zBE)s_^cbM_u)xQ_1ufjDd86j9MR5X}PoSq$ka96sV}hyit`|j6$7LhYXlvxM2Z_1t z=2~Xt?*2HZ?GXHr$$eg&Z$XXQSwB?n9BSq9D*3POi%cmOvpPBGQRwGk@GIbEKL)REe-BL zh@=Cg0;v_dDVy@f90=(j5^(UiZn8n!GCerMt|4xUb!=M}txK%d&3=)-m?Q6z+MKz% zBv^F`o@#4>gVlQ25z|N6=`s`JU#(j{E{U#ocle@1E7QiSig?7?_4kP~t??eDJ@^1f zdRK_%jkI^E!cXO?KZY&2Gx#kJgZ1QqXiEMx_R*pKeErx*i57x?C}y>#ZXe1{CY!$i zgltx@kWDui8}X6M6K+S0rRZtX4psc~(V&^XG;_#Ig%2;3q8NV8P-&njekH4NAf^<> zsuGev6FWz@@Iq0@^5zVMg`)813)beg z-dI9>@YbHj8qNWz$;PvGrq5iK4s++6Taj2FiN8h(!BWFQ)rQ3v(Q&$f_KVsX30d zl=F*`UJkV2#T-P>cEU?`ggFjNwyk<9@x;l(@U@k!n+p~IhRgEiI4s%b zIgXPQi=W?-<~S^6<~V+zCCwbiI!gLRi9;lS<~U?~a~ziTuyY(rKAz)H=7*c(aMYE5 zhDNPg>5Q*BqV-`j9db0!bQrN#ER8cA9^`+W3s&j+a~-GgZF3#wAn(jwoSyiZ!~BwY zjl_=5P?)}%OXPNA#$z{=#WV4uUj6}VRB^)E2aA#8tAFAX>q1%i=xnTvH`R0HYmd!U z&$j>-S*2hSGXX}sn z=8Ee=Vs1Ttqmbs~_?4m;Ey8nz^fBS*an4jZLL*!xt8x~mG{U-)lKm8aBme>`%iHxN zEHpyh38!8oC0|jBPEURFJnf;nSe{BAs}~TBVnRggTICjifsQOs(k&AOw<^s*N2}Gg>OrN5R1WI9s;mO;CZQv5`O4|2f|lj& z)Sa!%uc;!^>bOzCHvCJ#iOFCa{`w+gnvo%Zb^^=xYP-?V>Q?62hOd=*x2@jfDiR~g z{jh|6g(4?h+_?n}7Zs}{sC5ElI3~Xw*8*58EO5=_d4r`QkYxEwZ zl&#UPKzgM4dw$=-@iaeCoBXN2xFkZqGC!|xA_?~DzN^bC>1{<3Rb3|@k8>3mC9;Rr z>C1pnr?0@Gt=)}y^W|yE)^+%lqF1aMU5T5sR%Tvp<~3$si#bvUQjt@=u9sE098=Zn z+Y*T*l~5!A)k~JwffN?i%kMzuBast7;)lz9*NW3su%A(;!FPx?*Z{+CmMwitm}dEN zf~9ji8Kgbt>_x7Bg?f z9I0ogSYXhfWmRs(6oWPtgAOnT2>^p+c|AK}!63h9*V#V16O8t9g+gX1y*MA*1|i{~ z(lLy&q$~~kZeNKP-AbY!(h~f~GMn^K&bl<^g{Uh8wS*ULKhd7Xi`xJV-$%I%2*-KM zyc2VzK8j+Yk8-!H%I%oyqiiHzxcx)|&_|Kw^-+Y?)JJjqiM(V}?Ws;-z4o+ZIUz+n zak3P_#!A-hClY`nkmYGlOP2m6?9gn8w5RqH{YA0(`As`VwoqBGF)d~KDEDSb(^hVx zq|Z~)L;|RfBHPoLmUfHNn6W=a%o!|#F`3}@6Djl74woZ!N30+?%Y-incQ8#?$$ zF}kb-8oMJNB<+s;xBWya$tIn25jyEUqvd`xAHs}YGhOq5c|3?2zwJ7sydt%H=wW=) zIh*8dQrVEuN9XUEPbUj_#TA%TH11B*=W?#86?)y$vU6fzn@cn5kIf}6DvG@ z?R3=MR>m&^8eT`e3WReVX1`dov~Lbd&&;w=7RbS+dPK>J`P}=XWF>WhtYhUeA(7M{Ta8osJSf zI!d;uqb%)Vbd(ZLb(AvibkvY^loiDIs-sUIrlaI&)=@^6l|ZA8@*w}Sj#5dcI!gCh zuTQ_KY;yAkAaui9Sn<6{Y#+;MSBUm1l%m&7X}t}Ym)0=DA``oIR8>vQVVZTdj=JU_ z6c^R@AF?WMVyfD1AznUDT)gFl7sV*cJEtlvqB>l2t~50|r>dsr1H5VBISPSe_JR<^ zfeJb4I^uj+SoEjAJto)3R>nJ^s=udvqb^5$P;L&e5F)$>H{19$_c%;mT8Gbz1|9Rx z%b0JL7drb!?AYS=v!_FPZ*Tb~z2g2~C}xF{6SV(R(BA)rHy%HHKEa!3A?knUjfO(= z=FLY|Hve-ub3ugTO=%e3%$wq=r1HPTT~YcLBWDD&%H>FYw>gY zTmAbG(C~WuUqdixW_5_t)aq_-_)^!Ddj(&+6imGm=| z0mp^_NN>yb^tPqlsJF+q&i<|;i3FP@NL9GV$)W0o9q|FVT6FJk~cAfpM@$>ozD*05;Cu`T66zt7P z4od;;3sz;*3U4W_L!V{n3w+W(=VrX$+k%KzQsKLBY@=9^)C&5NLu?T_nA8p`XDWVHZ*qeW(ZP1vep%SSmp+>Xudj!kx9 zmZF8alE5y}o8c{ozZ|~v(wmvxEDbAye3Zuwt|64YRo=9f)OnL9O{z#@#Y|3LsV;zMT zi9)SO**ueNWM5MPsD{T#WS*(0xE+nmGi@yzPg0mj0L?SW_IkfYWUHH1pk1^r?nXnc zxW4+QCCW6|hG>FqG1!sL`Z2Mk#3$Z1qP9(uMu|~CAEXWRR0DdDV$cqR>4iES$O?Jx zUVffzira;t!v#)c^YMICA{$onh4A;VWwuZ_nI+%(aXO(3@8QIoUJmV@rYA?(Yre7M zx1CU_{4!?J+p^QPD-;GMyw&P$&nk~ns>iWs9ow$5a!%W%>vmwR@hjLL9~vKz!dQ9TRcm5&xSb24Um)fU}jJ0)?3w>Zv^cYbhw5evI0=181Zig120 zS(QFaaejMoerMyn0B~NG$9Z8jaK4UP7OO=Ex4L|8d61T1yyHPK2j-or6D5V9JLA!N zQ1dI}(c*x}`BW@;w2#N5CGg5eftgF1ISn(sG3AUt;mmY{ErmG}XOs?{Sz1=5A5)y! zA>)hya7LEL8DWjY8H?4TYvhavi8+(-<&vAyxk1t4@nvl8pXC1_ysB313z;*h-iq@kQytmz8BzmcbNXb`)QJY7!#=e39kxMOY1dvAS9( zHWszT*;vUuNLPpjsYawD>6vCI|Y#O;j{}=K8Ox)FaP-5{^+3J7$4RG1RvJH zf)86jBmJV!#13}gu&%$j$WVOv}gmG1)N{^<#`s`Y{^{ z$x%u_W*1Q^DPV&+0QF;JdqrfVwz?vsA7jT<;wHha$FX=VK29pu9o6t4TsCJ1D*QHK zW%#o$Kd(&?|I+eG`Xuc2+(a}-8wr|^HpU9BgcoZ2#V3)6;;eGT3!gml;y%RPe z7cs6o)XMvNfGO|Iuqbb9Pu(=+R`}%0zhMfXy0x5c+x8P|2CDv0-@VlzVGUfhL~gHA zw4l|L&GE3?57>^UFm2(J1fQyo@D@UezHjCa6ioXfJ6QnSmXBHABD?LjOVXz2U#7>I z?U56OI!Cmf?8>H?I!E+l70~r2N&;vICEMF5DLl>**+$9OzpIrYl+GI30m&^1kdKtl z;0mbG1c-9VYxh00ESxC|uEX0X9MA5`ZGiQx-eWSMaAB`PnfJFM^HpBl<+oegxA7^G zuQ6iI;`qAlRL4;g$(5LF*(HSAMOFTk!$b#C)#No#u0`U!hI1uuq>?*dF`n+)0pGNr zvwhBGb|r^M?p|#pDrDQLV_>e8?AntuV-BYn9@g7CJ&|4~JvkhPORh(sZaJXv1nda`Uo`GGA z^yTiW_zA4zz6yB+M-otdf9=`VzI*M3INIvkP7sO2`~-`-c06IqyVA$h`xLw2S5;PU z|5$Ei2=}gX?}*`49(%5TqFyPb*HNAy_T%wWc}ygiZN>%m`Vu_+DHK_a(%{l6RN5qo z44UZOWL0*?R1>|M>XYl#2|yDq%d1YpA}Xs+3@)|rxUO}!SS>m#P9Nl?U0@Hw#d1Kh zyPUl>UJ=4QE(V5g<8zi!UOFQw1Iu70?EPq0=o@k4a9r)gDarTr{&^s zr_3u{QYSCUNBfX2J9f?7&&>VJJiyFfUPO70Zu%5hfa z6@pOaw)|chR1DvvEy@2sw7mzMoK^MzKg@1swk5FHmO=?7;UR%g#oeVPRHZ4{Km`;P zrN{$2Aj&gO2)!d9AiYcPMSAbOh}49tND-+bAPm3v=iH~Nhw+y{VjYz!m}BQe;M0+$a#}0>Rz=i}B!#q*WO{O6 z)me6OUz^Z1IS?*MOx98*0IcKTU}ZYFo2t{!`xB4tBnejr)KzMw*}98bAwB1R$*AZ2 z{Vg%Z1j{{JVwNJ^tl)Z>a6EC2nsoCuQ9!WL-}Giu9=5mjy9L9kO=QES=ECuCA?iEJhvNH2!tog zn@d)xhPmV=rp&SpZZ2VO7WFT1JRwPh+1#icHaFUlUo2;o7zxS5Xj-OoL^3bx#w6v! zjdv8%d*wv=u(?p~us973uK`#R znE16st5;kmZS5p@_<^xM2{bD{hupWY*dWQH}7B>ioB}dR^Je^g(5% z2Eiv8A5R07mzA^lDn@6Rc_wDgz~OfUDI{g(Y+2RQF;!M}6$8&P1`0u0k>xQ^p&H6c zN6REmr_iWPVZUb~EjJ>rYd+&J=N8mHtK#wjDq=xO9sj!8I`j$_M(d-L}nl#@DM zy$fll7_ZI&mGkO+zSy^D=6RT-@k$}#)djMu=VFRip?Gzi@k$7ICClTLLN)Me@svGE z*JICnVvnOprm;tk#%jd~F$x;0l^l^&E6H3(xp2enS-m34Gv1{2g3j4KKrT)-9$ku; zXwVA6g*e$}iy1xvd6d?Ral3xM4xQ4vUIt+5plW*_hOU*4SMfNL7H4XEEL1rdDsv2( ztJ?V(v@mn7uprvkIc?76xND^ovruz|bYV(!t{mp^BzaucN}rIyZV13CKfPEvV~NFs z9kA>UG}TG!Yj5vWh>ra9b~@EreBie@ND5~B8H+kknp61$BcH$9=ur;y&&huR28so&B3+Rj^l2G(Ak$&>$#6A z($1GD6j99}D!rN`cwJ}TVzub#?AP5X=wTY}6nyyktd0{M*_~*)ZnhRKflp@||L(v` zqMJA4EJnAOc`Ii4xYN+va3_j(70$Hwc8YegSY+xsSI9)siqW0sGt=TTGjHv&P)>{I zTspBtC@9rC7RHyhh$an#*|*|nd_Ah=#{Km50_^; zy;3OjYQ;pa1h3O8i`Ak-uj=k5^f1ZYglXIP?CY6ci4N~fSbO0$cy+Gv@L^DSd+<14 z+`w(-W0>J@lv55_*!_Ad*X2|pX%C)|Rec0g?ZIBkshvcPgrGf;RqNs2GM7_@qCHqe zIc1O_IQQ~&Ku@uy@*lMTjs}_50?09}fwZ(O(Z&Wc$D|Ep+PA#;s*E3^HfsY^KfOu% z=NUhq1eNpSX};J_Y35%sN8^V=!jETURsVu1e*9MaI7+#Tgn%EiJbozDaDI$8ez=q? zHF3pJBGb4c$1tuK1(tLpS8`0smAv0@<@Fg~M0Fisq$j*6!aYsTg39?ak1uwMn)w3e zXnavf_%dHs^*K!O<#)NB2mxPYd3;f*;e1K-q?rzW>}Zf_9Fb!fM=Whiw2>n@CgsQo zdLn8we$1>~L^Uw?MPON5sk7N>yO_6k?u_b#pM+gF&5zK@q^ zZR_v8gOfdWX1G{~g(?N7a!x*& zr!InyQn$|^fQAk?^yG* z0S4(`(49y-uYK(GaBbdImfsKYlGkPb;j0+^+suzKN2|*elDg~@S=Em)RhLD1T_yx| znJlj^Q>X>jWi6`991Sw9E|X(eU1n)pqK$P~jv1vc6SY}gc9om=;D)JHX^44|eKlEQ z;;+dX*Bq~I_L%tV*y`(_$90SXMdmLuZ;^S5&j)6CcyMr-y&w0$C90{n*zaGp#$EZ)*&}FImiq&og&V z{3q;Eq%XJLP(>81HwX~yh)2b*eq*ZlT`QFf`vJR_I4d_pbX&=->a~f+s0B0H&iLUd`x46 zl3jLH3Pk1f?u3rCj4)wEd%rNA^D@$95p!;Ug<4=4nWQq}Xpm`TM2=x)#L~7z8_P(J8KsPf+N_M^yd78Qra`;O_%azU(MG6< z)kncJC>xvt=Y!K&dMQQ?Xx)@vd5*yjyTCFO+q;N_y=IthR)K(v^5H4g-JW6zmCYhd z;KNNntWK(xd-11rGJ*S!oQ+0J?OkHZX?@{cBpNRcR7+v^rLiT$y8zO-o|Y$2tibDtAIK53Gyl z93@x^D;CpnFDg6$+xpW?1+i%)b(h@+WaKro@;JPmyCT?3=IOkZJofHXS!fJJX$FxXe(m8@eACl+ z^_t>U2+Bq?F6W6?Q!LVvrouA#z0Gt)__op#U#y{}m{Q*??VTyi*9TX*k@mdl;#;xM zR~WNsL14o77Jh|}-R_0AgZ#ptz)!v0-uC$$VikLgfWD62u{!h89e-Wqur~}VJfGQ- zctC(9zA$h7cMuw^VBrPA_Ty?9{1)J3Awz(Cx{bxFZ0gO{G;GTRF({dMRSdSBD2ILR z^RH9fNkY9GyHz#OI-uEo>kGRuxd^y5HcLvJ4j*Lwws%c!!X$p-EH8a*^w39Fb8iRP zlQW@zdGus(#V>hw*7yig*=ljuEcp~pNGY^)y^dk9&xznEiJ)wIK&ck&PwQp+nJ|Ft zoiN#@>#h`XEYv#kbaXTlC#FxToQtaLoqcz4SYsC9@6P17L~YjM0O-x&-*je{<+s-; zEVlJ4$==Feps9^&a&>&glBBznk@E9An5<8EkHtF(K5sAA=8L^?X0C-fT6?LGw3qA1 zs;+^l_VOTA7;YX#2--_oUVEuf3v4f^P;6sIgG_5L! z?b35`xG=#?c*~5{wW78q9M4y z@x%pY3^$ckT@O<;h6hXjhgn=gFk>joyW>Nlh|2ExU?zH1E-&t5%5YtRaAkysVC>Mt zPf||gbo(uO>n0X0aZ8nn#qFnfi`*WjIeD!ve~aiT;@36LTO&&qrSwn5Xm?=y@blKW zm81$d!?kIc=-4B>feiw9Jj<@2g7=7Q&A1L4`?~N!V z<#=mZ)$d`d93LV%o3N$UZqNErpJ?`o>Q(=NZ7}wF# z9X!p-!oa=IT#h!ik)Hgwgg30MeV47SEdLA;zBad<>4OVF&9ro~vTU0&^M}fGgjTH4 zrp%wquKoy9n=%iR794J|3877yvc2N0aBRx7;+(*FC%nEmi;$!^%b6DEG49Z#ZLYQC zE!p(#No#D`VC7-PxZ<3?J^2Q7sioO%gtDwVp>=ZHE6d7LR+eX0K2)9m3t(9p<9(|` z)Hn0X<}dM(cJ@*@WpO9P#l}2g*G$Uyj)H}=T=~{M!B3&>QPaDfLFHvL@R&^IlmCeH?mD5csLQpnkd1X_fhLz35{dNANc!~&DJXcfkbd<@o;wi_l z;%O8aDUHQ5$7IDbE1!4VpOsJL!YiK>*oe-(jmD?lL3w9VpCF5yBSgApdx0rN6*Kq1 z44Ivq5zdC$+8ePhc0Q0%Eb#2NvZ_Ov;@Oen*_nzg5(1vds`YSbpj+QjD56@AsI54s)Q_LVGkEB^uTzfZE0lG8(iA-Dqfyf2Yun2HM8oiw5nYDMmujHp;5? zaOt8)LsPJ!XrOHroFYf5{Eg!Av!9)fwGHrg@ycu|gic9Y+H8Q|J5QVq@JB20GfZiO z&<1$f-tk!Dz!-P+3LD^c^@?2>mAu*EI3Y?ZB^oOylJ0~w$;Wg?4(qMey>o6P86O&N zY4R}XElue@Vs&O8g6{5JpExRp|Cbc^k*)ZVicP>2Xi$2 zQ7q`szOt%SOzF=tV&Bn%-+LN(~mfM#FnQq9Ds;dlLYIhJZmydTjdsWSXN zR;f<1ScG8sEz6Ucr8@jb^FE5j&u*rwR`r{%&QA5kQbx)4&y%L=|3j`XLa6GO?P;r} z%{!CQ)&y?X6nG<}%9$l>(GfA`i%GTt}dLg8W3TPS>exfNdB z!_U(fNzl`msf918Z#bQcz8nAwebK;eSAM!gAS7@z9*WUHAYJ3$_CizIRj(cl)EaD* z>Tul7A3F_khsVLQa3lDkO6eO}g`oCTNug9n$f_QQDWy7IJg!)3LZDQ#*u|Vy2nt11 z-xQ0flmu6$bSU6rBy-?R=&hq6s~A-&R6<_J5zAEnB=|m+yWQhx*N=bID z=Y%>Aj0uhVdV-| zx~3O4Z`xxGUNB-h@>m79O29NO_Qd`HVC6>hNmJjn$C}RXaq@e!z*bVFYRZ-IDK*rvE=wZKlwJ;} z^!_0wqr=i~yvM@BjBt-dJ9?$J;XIv}He@=lJI;Qxs4oVNG(juX@Jzzn z*g-wPTv60aTY4?=<#VO=2NNilW=BE>Nqm$5{JP#;ist%pm1-H63fZ4SPrR1-LjIPLr; zS-R{(#+~1@Yp*8cBNny8F7;A;r2;t?Q&Qb)H~Q=YX2TMl3o- znT%Lm-bG1eQK_d1z1y**JE2f8Q&u^K*DXewQPfzsc$g9D7M*`wx4v$X=6Jf=Tlgo8 zebIFFDo|d#=8apf15=EyG4oo?(ezZYpr_Z%s$PvLJ^hoIc%X?;2=r8zr>6?lsHd&w z-%Is8sm`|%vb1#zX^zbTOIt0~miPvuNm4~y&sVBi=VbRY2((pJt>?v5Z!A^L!%J__ zR>7sMe^xAhb}vxE-@%vZ?01#&Y-xma)>1}iZ_JZMXD?LJuKf{0IxD-@^HN>f&B6DS zb^^C?ixQ_boFY{*zIP||=)-hG4yPmjV8!S#hBppYJWMiJ@$0jz;m?iyy!J;@^K@i< zVG8({O-F75Mf-Csx$kc8?x4NA1&DPQ>l=BU+lg-jSBzx3HFvEodm%#RLrVjDWx!%| zt0G%K%c0gw$s?fKWmRv+)MWof;`LmMMF?6BSzZBEDEdBDKp8uzfLhsXceIr&XVHUX za3HCbL!Q>ccb#)6EljBc5;a@u3F zb=X$#eb3fF-M0810<&ct(@FFKW4pfXs)S&E6J;YZpqGbCq7CyzD!G;ks_Od{v&#iT zvP+bUJ1rg|G$fPl3DvSuKTl*rrKmXqJDW|e#?gMQWV?-W6$(~6{?uOD_lGg98=jW9-iO$oJ?!r;v|6w}v04Q`sQ|a{g zUJ*mO<3|A&qlYlVOX1T3CLk3Q?RZR9^+8N&$7SNHdLYF}2(&|%ryUA4Tsy8&!Yy-_ zmwF7UW_ko~#YkrO8Y}Rf6UL5qnPZ; zv8%8@__s`sPk_qH`qNI1PXpv$Vl$t@9IdP?0;Ktjtm>1PlI9iSnI5fXdpsbdDa(_l zLJgPZIZm1@q=h~HE8gk|8AW(TNNfD8f;d7D-j%uVgg|(*JmDE33l^RTs7#aaj3?a* zMfxz|$uV4bMvBqUC_E373eRuzHil(e)XPpR^<+1$&;@XbUt6j=mj(9 zVU8v=g+ypC%Bnt(DWP2@Ub%Wx2!tlf6PiK|7aGg5RM0D?GOKTGRX5XfsjMg#ItpbP z@8lTXtQl!WQ)9E{VUlJoTR(a5iL6~yZZdwcPW%$z_6~_TpcUB5Da;%OOWz+!?dTQx z(K{r1@8CV-_p5+dAN@O)-n70)+S(oelTWO7WN{z>%X%GlKsf$~pdOIL=+eYBW+FkZ)21j2 zr$j9JN8p>aOT*`XEkmvGy9(zrLDk{3w#CYW~uTD%1A^iTUq-URj^ zeqK8zE%M5)D~s$6dC%a>vc;LoCcU@jeafWvk6&zi#+%|x%nn}CzPpRe-4kDkU5fO@ zgwyy!a4phgAGrX*D|l4hrTwatSDIM(G3ERNE9d_vIm-Eeu-FH2G=3-=&terTON(Ql zgW$SV%yRre=H_l&-0}M!;8WaQWuR2RfS79Y7S?tKtb*6=0E?d7<=OyR{u=JQJK%^BL0$Z<*!Tmi}Y2D z|$hmybiutp|(W~Th*DZ?5G=W zjO;AO3+0?-5#xGsQaAZ?B`ydpV#xN&wXw47)^%9Kuyvi5`BMo}|LVO|xvSOjY(lNs zfAM}N8oj(nZyb^exNd#04SUF$HF;(kYUBf`>bp=m<*BnR{1>qK>ROMG6k|h)XF_cu zKdGONmQ-iEJ6JPcTe`AcWD<3cn;K{o>0nyVFas z#_Eb6W3Z`c=h)!K2r0Wy#ly4&+klxq`{$H5h|t`}cz)}lhivjIu+y#%ei`3vKrI}B z&41w|esAOdB|aMWr7cRiozBmzM^s*#f^C(e%J#yA>4g2n%61Vc(`TARgj}-ANAMo3 z3kSH4SaZ^Yb=MNhGu@<;tZEacx=A-kb>>-kA?PN_s`V_KPM|0hQLRZ-T9p*Mqh&}N z8ppH2Z8ELx_JnQ?Psh3VtnbSmJdc}M$NAdCGzADdgYUpSah8fD=K#hKXJr*AW%JM` zfZUMrbWiFoj%3LAJ@&!v9h?HXVooLpk(6b}@syHh z9$%L!gvjG_<7*j|H%Ipmg3f{I=+js*9I!F74Rf@9sbYcQ<78D^Fvaj2#qbtVDn>%U za9LhaRjA?p(!-Q|%N$lI+*(iEj<;eYvpZOh`vC`jDjz;&S$#ZwXlRtR#tSPBM;V=i zn?%{J7K;#c4rF=tu~9a{=pyb=JVr~XE;2`X09ZY3kJcqU7O^WB?eR0CiaCl!6rCfB zIf|P_mRt1|LUR-fuxIEo2|I(bSA2GAC0?o(VPGH&E z>f*ft-I5vf@0TomO4x;_Y5lYdFNmTCOYaQo;7d7}x4zn^USN&eAaA~b>-KE>GS&Qg zY6nUa*3fPnTG>hXrzmeJVBZ?rC|ZsVp<6bvbLI>WUXl)-_71D{uirZ?N@)Ej%c^!` zs`bA`V%^-zfe^I*vb?^vLN)fSt9=07jtrNJ2@@BaQlMz}RgRlTPiX>Zk+`eTbuc8B zqXX4{)NNylQ|AXbiiFQ{G}n9_fXUe&AmKrD6YSj)m6=1Q_;cTui`IBiyd}Ay%-pJ6 zxM@HkC^NFWcSkH2O~GMArCnZncZA-&1Q72_vTSqy8a&RLF_D!tV{#^!&)Fi&M7}vd zAz5g@0%cv!F5U^~n{}3b%f}u%VA@qKNBa>wizB?L+crXmG^M`a4B6?7=qqQ)`bZqq zB6+WgcYwWvA8mOx(1X1JNNiw1E9z1O}p3VRTPglW$r;k7>70=H= z7o!z`jmFxU;1{&bd_{1>*}hT+_1V6%(n;8^G>ef!b5FRLh8>0N$^dL%RaSL*OtJlT zv3;E7LI~I{%VWDjHL$%HDMNGze-8TxPJ|QX=t!!c)^KI?xi$P6=5W%&8`V{PYw2XU z?`<4zxliL9qmAXh)rdGDj^(~PMBMJC6GCXYPc~OjbH%n@ePq-%2md58E%ym-`@HFB zEjK_Q>+IN1=vkkFuF+kJc_n(jH}ZIDz3! z(xF3W5p2oW8tmD`U}I&Y5FJ`aK^=+c&|M;N7x{^VkPgZAnrx%7*)`b-JW-$CI`y&m zEnd{8u2u3d$uWj(O+#&o-YofRz5a-SSzDqvKfVjT#q?(uDD-DTEcEA3M)|sYIN_hX zHr=-5WZ8Nd)EEBx8Ppg420&BcE4Ji5Rz<;~GBeG~C>kKb(149(RcB*L1MU`kPc|Zj zKm%lX8lX_aG{7iO$1GI?O|O+*{ZWi^qAE|(iqL0rW!YapHe!qlCfbisO`o1A@i5K7 zkzne|vUY7RRWA!=r71W{KGlZ1w)s_#$N`VOg~1s+GYznD?%X7IZi+K_4PuERi{seJ z`qYF|i?ISuZHg63t+WBK%CD$7Dm%LBqp&dt%`B?TF~gk--0vTd|2P+nBxW6m6h{OFoxkVlB@5`+;y2rL~{(d77vNNg0O1&jg z=013RVd|JE~ALVm*(#}-E!;A@r zz|_C3rab~$?(^z*QPmVw%xfl3=5n_(c*9)oSY)KR+$(L~wkf{C?BET}ZbvZAljS{{##9zacJFHb)lm&wvyondNw! z%w3FriaY+12iRVK9}6(C936pVZGriR@vj8xjd#H6--|Y^SlLAJ{!-w`nTkFBVFqx) zx$GQ>7u7c59&pP#ukqHIXBS7510qEfiL&rm^>d$R9@ZuhW3iU2)y-7gHig@PHfnTI zjt+z7BsTdR`hbARcNS&v*h1X0=q7dh!kU}gRVHesO{KY-!>ymWb;!+x+!ONs2#s0M zF9;pphp?&%X_s@q{|>&*Sq7HLbT_tp^hz#_NXZ*Av6ngLx}xo2xHi(2*V!C*hLwdc_-SQr9D4mDZ0*VO;fDj)R<|9o^(Sus z3-{;zyt+VA9aa}`uzMHF$xdeOEI(lz^>y%fYXv5+)K_?x#Ja5f?g|>^+6{~H`#aJp zR<=+%+rvW3EXOWB2iYA+?4ovnApw*MC&e)Hd0!2$>gRox813|)vZ^7b+UW=v%b6mJ)=T@JQDd@J?to7kODBl4nO zV`NJwj$oO!(v~V&dl8M+KIDt4`9VmTvy_GCBa&%zrkK|?k%-!(WMTImnsOtFcIIj!d-RutC#JfA#f{zo2Sg!7{N5n zm}gvT)HDy1G$QF))!DFhBPi6okmeWCKk3tnz|Q06>61i~>67$Ce)>#Legi7sm!u<0 zl$Z!02OBX*+n1!EOg>j-RrkWwcc)RI*QfR{~3*ArZJUXmdfjPQ8ByZC{bAQ5upz5I4^!m?`*Z zXV8=s@;^vcbstR0|4}j3ok0@<`OEUopeYpcZ#;vxKVV)hz)a4dc{CiMaygsACBfF8 zK{Fa!o(dVL!#=XMeOaXqGmM35Vo~vzYL>5+Aa} zh0tPxZ0`)3r9RRbv;!=4(U6=$6CgQ*CeCmM&15p#88k;!IX+ZzIgYS=`-F&cXV8Su z@~v#|44M)3f1N>dG7(k5+vt6A;LI^B_$QEIXV4VF)WAYALcib?Ecyi(;g54}e2g|pbRziS6NV>cP=C~avf@gnd=xKFIG8EloDdWC zBNQc!DbDIh%u!=%yZ<#%6LoYXPL)+X0aG1`C#AfH7^Onck&spEc`u!(RVbp`hp049 zEBKs0086jx6q|SlNv-`{{7juIWbhZF^=EP|ZA-LqCO5~VGr4K~toF7A-s&vB*WQZq zx;fnr`ge;N|4#!2|IfvO|9>Gob9Dz>b#aEM8s+McGr=!tuI?;w!$ov<2K7aBj?zg) zq%@0>V&>eLnF_-pEX?3T32NU&Q2W&hO4&eAf09)_9aDmON`i9ddxStxvT8l=r-D)_ z1hs!6D8a|J+7<1^Na+r(#HfNdD{wlZ?6tV?-DL_Tt`nQD&CBKJB%Ak@s<(ze!yHby zH}5-U{FtDA-q$Fn);>>(IMS)L|0>dVG#wK{wYF?;-q&a!Y?^=j(MyO!ek_Cvd=$Jz=Ip=Yv9rF2_QL)GbDb_A?#25ZGusbP@Ok z>CnaCoDTW(y_aNAf6DLD4C-svWk6HS5+$6~GE;FBBZc938|G-5rOcsOSIDYffGN#- zTHf3M^iW56!8Y>h&<$0;FI6+zDGAuh$*1 z&714(E_@68lBPRXfkJn#$3l0mAbk!<@!+0S!DJk7BV~BWcFLh>MSnl`hrTd=Jz z-xEXFRR0$Xrr2#tGyW?srZta8JByXQmC{q@!oGY(2;O>hbmf6w28HtvoKiXWX5K-oR=v2#04$N}oqKFj35nD5ZX5D~dq-?mU z*6?#?TD|u>4};{X4wygPX}}~ZOY}a%PaRa>c$x?HoVC(kQeoS8J2sa0*80g?(ON4- z@^m;!7Ne&DaApTHoQIB#v{0yitu#Xm5YNl5K8C3Ui1{jOw(BnvLJJVGy;eivSb(s- zJqf&{W$;payw&(7F)_=aPS*%isq~Gc1f$L4-kQ$inodcI{yc8Uo5!V&QEr*U8g2HL zMIV=B-FDmh+1q;cwb|Q-)On2HKit=`H!~8#U`h5aY9?`8i=7-?${-Whx%qSLvQ1{y z+Ek4j$Bokr8;?X*+0u-M850}@28BrHZ_h+GUWS)v)7RPp_gri~3n{#ZvZ}$#hrsox z{{P;<4&mqZ|0Uq2;P)qcmWjiZ7~76*_u3`Pq$yt&a_+z*azM)6*Yf$ z)%0}B;Gr457`>>V*$zyNrSdyLq;pD%>is2I)p?kz-d~i;9cZx#LG>=noAXjAqN?8! zpOfkK##i2c+X+ zL#KX)%Chcc)cAG<NZG=I%M*utmh8-a!#nql|Cg0l%QJ$-lr27ty;J)ECivN+%JK(&QkInTn$rDGVoM zFoS{wbqpMnppM1PCnA&$1oeTe>f4wS)GHE{juZ022OtC`tJd?MR8R^uN^Ul-!T3Em&S7Bi?X_@)f%3%(g> zDtN_~j9<;^#;=Nv2U^TD-=nC2Xha1{@?0%oN(KHd79VEqkr1eWEKda#YGf7g$FEjn z(CZ`tQ$f+HRGVaE&htopEivTG` z&TPXSD+cc-VF|ENLL5w(l6>&|$iH4@X(pgxGy7Ftrr}3cR zQ#TfT`Ud*JT;8o%u%riI(4Mzgx$u!YSD+m9j%N-0aZqP|my%{QmSchn@a&!VCBbWN7EBm&xD#!4Fno(d$HxAT1OnP$!V`O#i_|u+*0LHAy&+G4raoM;nopT%H zR`UQv`qJ{`l@=>3$JlwmWVRd_%H#4_)O{Lg7c0lBjaU%~+xg9$ff*}~a&$H&a?Z^3 zeB#vZ;9G23u(M3T$<&gSk2Pgr=L0JbQ~o<9AIUBr9)cSBAf4n;ltxf+5|#1UQzs04 zVyKD5>I$HOKXkBNPrC%qo;qo0%A0I)3;xbUNMl=C!$*j*CFBBjHkg*9b0CGLKo=!c z#xehx96+2&B;o7KP1yliyClhWO3j96wi8uZ)_ry>$*xYv98dS_UlO~!-B}2AzhrxR zr4){}2-_>g@`3hB=>j{YXZ@DW(x6Iaur8EEc_7PU~PBPb9K9$+T8A@Hn+Q} zlf&KObFFlO+S3OJ7Ocf%-02P89jXwF50?MVj`|0pJxn>^t!%63(|u3Jpd%|omDW5O z_dV(AVx1=RbW;4y-jSZXQg>k;@Uu<#RtAOct%`;2ZAF~r=q#)8R|CY0MVP&bR9D9x zF0Xd@4XG6s_nX-HU{P_=2K39Su7asH;62IoN#f)4`%b5!Ks7rNN@$ zl^^ls-`J4bYEZKNezCtf{1;z#8rpWm*~w>Jy2{xOB5(z3U@Tr+xoqxK&OnDhCKAG# zpl+)ZRI?LQa_FzM%fEKcGGWwn5tbcfO1HOrm{M@OZCYOlCKn6m7B+b|6~nl~mt(Uw zSUm9N2YL!S!uR!!?*pLV`x;p7oL+HHpHo%O0#J_rWD-~txAnlo0H$|vQbYjt!+8iZ zck^(`7vy(rIXahMEvVgE_$x+ho4F2VaF~kX$rN?l6n+X0cK}H9W1>WJva@AZ2Qf7# z`@Y0x%||4J=455pdOl6%rf|&3o=Rjo03^6wf;&;NE=w?$^jJs__8L=|VOc9i${-9^ z7o)2jJl995sDJ8H8DlQA9GypQ+$3zeiIrWelo=z~2<438?OalsO=)_VQg8wWjA#n(bdhLoO%wRvG})~S3fZlX+y*6R{_agu&OyuKDRf(-(gvU9X z0!Sx4mFHUNEHyEwQ52`n6*Blxjh&XjEcSf})Ee%E*~v=Ip4hH*wT8dL-CcQ^;Z3|b z;W+iIbo)ByQ#$O~I3vRQd$a9Ad}X?_Jx6pva)8Io?_ma=ln&J&lUEAt&Y|xZ9X|j?DcKqexlTeSk?Sp3 z#mdFR7;kG4$SlV$N(cENkl4lU06!8y3Gr4Erkl6<@TmNqvTZWx%`fC9&lK3GbOF4{ zn@Fgtk#3W|<>-8CMY0gVBIKwXT?llEMQFWiy|pogqIGI`&xpXa(j{b}ay#{y14N1~ zTz5lsbL6+m#0_ev?POKA!c^__k!q(K#ga$}YA0FVs86A&o&2}T9I@dVB(=o2a&!SS z+C^xNImtAX_~f>pKR|m|c^=O`o&P4?@Qk#PJ2PwKG|DhSY2{`!%(?-rnZ4S;#8actOkz+NsK$ZCdBn+ykq|6qH9{H6m>5BW`@9|CTdT<_m zf;~Msq4KuF{2GkAq~&0wl^!!m*t0gtQS}b(uxNqpVa5(!w~#p1F$l#OTP99!bXi?v z0rt7lZW<3vO7v__p|a1~O%KDH++eKTRAb1W>aGhJtLXkHuYHuXn}XAv=X9LI+XFuZ zUb&4_6$ft*ti{^{4jSG8w28Yj#ulCe_dP35+v6Q=`vn%|X*=MYMB*d36VDyKS}f91 zdDA4m1L*KS6871MMNQ^8l6BT(DoT{{m$Irq!<15fB2oXuq89?ClvV5bB9*j45!Jaw zHBD0hLbx$K)p0GhNG&#oJ9T6F9OK6H3oSw_aP5S1vwa&P!W*woDf|VU`+?>ESD?uM z&RFDsap_`ryenV4hK0H3?y!%gZe7^WZPzxm1b?HpPHv)6d!8K{kROM&2>MqgvT}3@ zMQPxd@@Emc{-lO>yx^%6@&aW8zOLq>Z!=n&d?bMxGP>~STq}dbPTKv6I40o;mSKoU z=Yh`lWJZQA#XCr*99^Uo|C~8pG922mO#NC(&Oo)@$WYH8)Q=3skRs0?gtDtUVvgt1 z@=qncGc6ZFXv`fiWk*wtCZmO;Dd(XfS;_G*Ey4L<>d!a+0WO}!&nq{|QB!b%d9}HlYr$`0y09B4 zbYV{{bm0W(q;1g@LopfxHoS?bWKh3}*dv4Htpl^Dih@H!X8r~<=#dmIgb zkSJj!_gh)j-7z(i`%E%uGjfDrBqyuZ^JQ9u6pE-WCMu2O1RvX)XzFp~rW{?2YB)+5 zrHJ}sw3nG4S+|RSY-IV2N1dEaBAElOATwi1p;2aA$vhp1j4qte%`%xq%jT z?%u$%zQ+%n@=>ij$95@o`1C>>cZ_IX)e_XZ+nIjE1VcaOVxb?V$CX#;0LKRdp_1JP zD`a)8Ej|PnV@gd8^zS5z6(fN{Ci2?i108Z(lgOne5rH^VVdj2j?r-J+n4^vOMG<;( zsI2N7OzFwz62~s4Cqkenvb+($LN$!|xovPx1UT*0ibJ*1jp~kEf;vr|D`fBmF~JLjQ z7If!mS=GZar8_n0j$42g0^O12Rr3l(RQ_Q}rgr@`DvQ@5q@~_|3QX&ULbc&JrZsuN zHf^hqI5}&pMTxiOx^Urdg#NkZ`xsE<`wv*;TU|h^)#Lc$2_-X6F!PUQo`@M9r*)(2 zph4Nc9y_mM6&cw-Nmlh(OlAK|W&bxKHxh#E%c}M4#5QTWbw#0w>PDhc6)QMzdTRxz zHMpXsFryzK+E#B#5}hU2mXM{u^tSOMozU_y$%=H!k8`ie_#x6fe)JSnpX^}#I2jcD zI28+i*m9T6+{I@DNmb%B+{Ng0GtV&dOw4emYWb0;5fv?*IY(CY6ijiZ&;e(3ijOlH zfZ&WQuToJcIO9(v+63a*{%yn^n?CGnW^-I;b6(#L%m(KJtO90&`7)~Dfn_YYvxwN{ zjOp*+3wUs>ft5!fnq+KsbBw3>YeTIA>W;N0w_`@~U01X%la{;2X+{r|PBYpZmdfx2 z{IrIwH8p8<`lm8H9hk)Lm0>Y2n}1CE=7c6bNS*Fe~pMyI66Qqsy)NxWI51nt73#7n^wr=4j1_5<}9L$*P`*DM=S4 z=_8eJBm|O{=L*1*jNZZRRzY zqt%g$1qNL!t9k{d7*rC2Ofr!WFi4hHM=BHy^6N+{3F%0=H4vXzYL;$mKSf?oSX!0{ z^~C4mMOsgI7}{XvHs!^cmu0*VbxlFd@gmi=Z8_=yyck`F8C{PNEJgZR-=~W?H=4@A zF7T}KAhKfKCYIk4`_W}t@KUYxkmPt9w{-*oRqAxzy}U7x;NYccj}JPS#VEs+>4lk< z$MHD#M&MZ*yg%F`Ubg0Rx2L@9YVx|3AjsG_1Z6xD@)k=>DcZCW{y9a$#&?^P) z02++N@QaTTZ&WuHpB$4GpKRS!dbl)vU%9@nQr@$&lS*z<@TnBI3bV3*bctYgp`YTJywY-o zBbumNO&1;^IJ)o{7P{~xbaF=FX}X)?_chIg{yJ@o-$e*cv|rc=2Hiu8#xN zGV7Bh%OJW~eF8vm6`a)y>&ph6%<~gDbkU)I8FVp@F#t6dXs#n)nzbca20IRa4;*kS zmCZvfZ*jGYvYKJ<6xLd3=Ud;}UpT{FVi`K9TN6r_jxci`BE?j9#LQah1!=;83?HV> z6*6G-FeBh9W<7k^Y~iUjC^mVL_cbJ`?~#)fX3oRIlAM<(DWsPq)7UP_>Q(x29$yPwqNTa2&zglF()MejB%pOWgat0TRmE84uGQYzQX1 z_xm5PQTMT(OD9PED&6m`zZ8BE-!5Y@=860f5DHvj8A_91)pBWf=?C4r+@I3 z%-x$aS-Q^M$)CJMm!~ciVub}amr4fSy(O#qcTDlFQ@q=OBzgA&5WJJ+HJ1w2 z&|C)N3FNnB7PCc3TV`sRb_H`xtJ6yA{A7;z@>??Ah!{_Y#@QVAZ;dx^gMv5jV8I)k zcVV|XU&(s#yExg3Z|3`Eet|Kch~f3`YVZXU-l^HVcF!wimN$#MamDJ2A3zLZt{H>TLqEwz()kcHpYrSCdi0@>@EWX77Q0%;w*)_er~e}do0Z*5?o@DmN`pZc77mv|ySuilr6WI8*u@`n13g;l)1<5CMR6kH1n z)JJ68Z~=P?HGa6r?Bu>m_8I7#S0M-)JZp2e+orHFzzps7hUfbaN+6Q`2Q5l5-+wU2 z%8c*}+JEo?bdI$DV3C6M&h6uc-+*?`R^jJh|BYX7yPHJmiH(4$m1L5aw|e_^K*wH} z;LbVsiY2cc;K6h|ZpWpK2@2Rhh*x&C3n^9aXh~kBSW0j{N=($26`;@j_qETDgh_%Y zAvH<5!VPR^AwP5}6kGp>7V*erEe=`JgipxIQ<;#aVnbG!IkKuU!wxjDt890XHUrRK zJABel9`v_&F=dE|NM>j)cl0WdKKIfnH~t<4`_>+XS%lv=<9W76K~k;TqhJhgvvS;9 zc^8_rN1+X0v2vU3OZXT}Hg-9-7%dDky3;De4xGhkteN95o5J43oNj7O^_%2pKciz2 zaMZWsv8Zp=Q>=VKth%#Qv2irD3oBN1xVz<6q_}(JR@BAdJ2b-O=nfJtM|WXr>~Y7~ zLfa9~()`&9bKHkVsvmWVxS{k@lBm47BsY_r|8DNy2OsoM{q}}`rV!bB&UJF=I*IFX zxP={ULKDf_4?011wG&hOL3>qo??LpuA^?c_%&P2KPg_?qAF6Ofb|aCEXFsUmOtWc9 zf&O+R0@*uu6WG1o-Z4wZ;%9Ht&A_wHr-vy8w}4TZPxp>#&h12gWq7QneHSFIK)pBJngJ?AwbJa?efo0D_y5kD^0XJ3YD9r)z2udoJ7mK_g7$$@glsF+$@k2UNUJn-R{S; ztXLLUe!NRDr1Nkdra8D9%=EN9{62Y{#gFk%*6tN??}aF|%6TSsV|#c#uzUG=dMmy) z1@|cC{Jx0IwRar^j~ZYG?;-`}&c@kZw}VXgrUOT6GqKRU{YkIVgfKg(P?iT4D>UvE zn25vO*En{Y0g8D#6*Gd>_?FS2K6JcdF2-YH1C>ppvpp8s+GIt7SxJ_<(589i=|B zHJC)Y;e+7n`k`Zz?c7NXL+{vS)3SD3R%>XlNnUGcJAvhmy(VjuWRf}dnoL&abQ2X@ zs6lA2iEK}YEO#ZRL+mwCrNOTL1H|qT^5|z_d`tLH%HFRonywZ<4h|~(A1uJfaAE2p zngt7Uo)wv|EX=Ofvd%Yr1wKzqlO37QKeu%MCqApf;?X@MXatqz$*veY5# zVNuuWfM=yOscY8-7O#VWVy%x=j5ffeer;3z`Y>w8)^mezWac+8gT+*$q%5jm>r1|p zqJI6Rtm)>B0z8aDpvoQR+YYHUIfT)U|-1Ul!vThRAiB&8Cs3`eIBaCX`RHO^%vIky?y7K zKy?vPawEGVB>Fo6rVdUaKg=uV1lCHggGg`tk*DmoAG^rCFL`Uz(*K9WvDEN5-cTHG z3ft8j%a@KByoFti-o^~4NYVa@%X8)t-2?@e~C z=aMl=?_c3)NZvQg!Rh^f4A1sFb|JPe4r+YIW$t4gx#DcY9Y8;(b;zgAGY-3{a}6Ps zgY>GOH;870+lA=bj1cp-#GG#5b+TbsQX&Wm(Iw$t^K9|17ot|RG?{l=hSxkb`WYcj zxa!iZyI$yjNwdvMlM8q(&Gzs;rAd+$Z^h3YLwd{8l+-~cuJc{)h)?n%748l{z#K`k zYGFr_tgHF|C&^mPZ;J>PuW1U42p>W~R*QI;Qt%NN8^@6tT2c5VKP}1V++06y?alPz zi=u7X&-7UX{OAO}E4KbLpY&@Ad&JLc6QsFK!M|OiX`3*o82yks%Q`v}`xcw?R|>Ox zL@!th4W}>NRk(`u{%G~ekMPR*-P1{c&7TirRmM|&x1);NfQl7W*+p1W&q*52agCo4 zK>}8X$`qfI8JZLDz-*J~Qd{sb6tZ)ksDhiN1*uMNc zT@gbU*nc=FX}9jRzCu%ckJ-UnI$?IVxqITfuuGA?T)Q<5rqF*7AowR9^LDGJP=mme zO}};`S@i4Q5LK-7q6~2b2%B-u+yyiKxd-^Q0Kf16y9%&_2M7iDr3ctefM0n4MI9-Q z5ZUu^3NBswW#BxmNO1&rQERrB2Xv$&@nHc62^c<%VTxVun6HBBJoH(FtOt+mU*z+< z4Eovpd=g;q44R(-2+moAZs`!)OLi*{9jnbC97Jg20I@uWYf_F{g0o@aSZ3X;fG8BP zC7j`dH`p&Rt=KwYV&9a)l!E>X>sPK~{?`Jq(Vk#DZGma{ViYOrpRwTWs&eUNUX@G9 zl|w0cEW5fFrj&d+DY>Rri;)mga@k&0u5d(Vr^QmZCEUAHZ}=TLxpZblN97%A0Cx3D z4hblI?sN>G;7W+EO>z~y(PA_-Zt?dp$rk@~4tOq+JkIai$ZQfe7XMDxs`Irk)~nx@ z!A#a^Uts%UG)M4+XZzqbo~f?Z9&6q8agOQ3Tu|tPHeFX1O*#Pk0gDx$&cOb1D||6J z0JBkB%9f6U`r2|}=68vw^P`4}p%@8Qj1IzFEVz4ZYPG8{I_NVjrYcS|vrI!M$>58% zl9|}ylX9Y)PQ$^ns{3N9(=c5^-jl4b#~l!z23g*OibBz8aMQ5#8r(E&PL{O`b;C$Y z#}Z%|S$9ao!*Dv?olo=c?C6o4>SZN5Gg;+pbcX;=*60qyiICDW!snTcgkR7a-O})E zq&2#6w(fMk7d|~4d=LC#%}QG;SdFr@qr*wT331;ib8#J+LG2aUZ>lonF@+4e?J=2O zUUL9kj1?}DGgNG`7p9N=?ZzV;L#Fj*4t9%y~1961{=$sc+1i|>S+_Q4@uzEZQy@xfb4TDS7W;4? zCAXM}`)Ikv?QG3%SwduMcG{D8EZE?2c#O{E`g9!7M4wK;2~PN~m6{7LLFYwAr~FXv zTLI8lJ`JmPIt;L&(H}t$SBH}_s3&;y8$1UI?evY*qW_hUbo!0Zqpu@Z%M#&nW84NaGX$VA^Y0?t{lQa38hfDi>pWb@>wK*K9nG&nA^}WZ+z;?L4Pp|OHZ6 z35tRS`E*6w7b{yPP|F`j0Q9|;rRF}0k&;882P059du<;!@>C4IJncIze^+O`%;YJK z*U8h8Oy#+1BG0q%6^lv8^BlRw8Qg!8TUp6u%bu#d{0$bP3qZ29?DRENz&Mk9A>WnA zzWnld5#U4zF2RWor1kqXMzin>s^4dm3SL-Bf()Vp5Za zfwM|~9kH%{KZe%N_4Dg|$03DG>U@hktMf&wuXFaD)*&}!{Lgew60XxZi$B%5wV_FM z{$==z#cAAE$Spo+dbR<+ygK9>aEbo>8E009G#AvzxY1~kAKoEjp2#K%cEf&DilCa) z!bjKPW4I37kU@PNkYB6K2N`fE5(~{i2;jlr(T4W&QLdgY$JYWwmP*8mwWU@Uz#4&@ zmP9pj9q>MRyv+-r)&UnwYVGr%!b1L*c62)_4440%8Psn`?#`f|RunCYVkBhn zjH-kO)v>#mPnBM!lI+(KD11mAJ8BT65IlkRv?q6~0`!@GuP5h-w2;=5W51U2jWh+z z3iY?e3wmh!?kPxati zkfO4D2fku)7x!IqE62F+kz35lbmIGj@XGal;1Zp90H>!DTnE+3nH+!%(up654|YDP zSm-N^S=8}BZ<>6AI^ixae5w=AJDr#T8ctc-(SxKgTqhpRpq}*2uWJ`tT-BgT6uBr~ zwG;~F`g@P!qNqQH)qk9@kwGE>T>pVuUP-6j2n+h6G|Gkwk1ISHB^vu@SOfyuP{Fky z_sd70dGOi~mnZXTDs+D_5SvgTi;4htGR1#-BWJe@)Ps^i%V>MM_8 zFy#p=O!#noL*JSjsys$VG8sB#jg5R)u3fuOnxW@Ljz0+?bk z^`x*a{+6VZYT;i;yEvR^V~f`KNqOqC+*zAt*<_U!F!~|OeH0@lK&x^*${}Yywonwl zTyy zb;%<}i|`AoOLifuk?NB6!h`6+qh9#5E;&H@(LVoDmCc^m(vDst1*asxS-!Aag&{t( z1(g|^%6mn`70rh$;jaon30KIZpD2)<3@AqOpUeX+KOvt2n6FTM7RYNjT63#-B{ql0coTE&f#2 zJ3@)d@XPp$#kEYPyMdWLvr@sD`-?W=hoybK_&BbMdO7Aq7{*=+Gb4nAMA zwRYR8MQL%h)ZgKhBm6s@%7ufEDooOx{_2hb?+&N7!t2EPq~-H{(j=c-8d)FmVXIW# z?ejI{D@K9@@2eFpE#-FDm9g`6Rt0BXl z(ygTB!e!TH`yNHOcm8SN!s@hPPZ@vy4GR8f(-miZ_(=IzW_W7*$uw1z%B_aL<0DaaxGS0#P3Y*c^1>6s>^kML4vZ^0rior9*;NvYpAz-j9 zPm>f12K)EJyQC7@>Th=W6yH|MI+8=JHq3MW+f4y&+k#PIB(w%42+pn>$PwkBtP+{8 zD}NUn59OzA87htGpY?Ur=iw&}PxMszEmRpo{?+8u z2C6N&2)YBp58rp~80BqAr6gl}HH~GVI0`_Xtytd$A@QIVGvw z1k`OmX9s<_dcz5r+QT{#TZBQ+y&3(%C#~X=T6L$a36qbc@s6ACI5P6b$C)Ew2tNs(ZsFF>TAkQgUc_ z>e4vet|;w&FlyN~xeU;RTz022l9)07E+rZXq1~ynYdwqA(OU|p0v*rpRKeMus`EG0 z1a^$ADv8v3n^@g+7nFd2qePi8et=P&|DE}Bnh@mzK_STJio>=u^zCE`B7b^ zWkRB6Z=h1t{Ih#II^84q3}rxjH6OHn6;1I&X1jL~A24@Md_Q(6(wEz7u`)tnuY~{& z?;y5YIepfZt5vPu+Z{qGhthYL%IhA0;#Q1W=`bLbZ_tf`>i~+;I5QW<3|1lj-f%ex zR=YSITS`3x+f!zFZ;2O?Rc*&qt+uMPV_OpBEe}A{YO-oQOJ&Mo9)snHYMR;&A@cgE z4Xlf-wZ4JO5w3wG1-JLTT)5!!tc?^EUK_cHz4!K<@uCwHIqkx7cP8&a6y@moE~@TF zxz=$);(i>r=sIzs!sM39i+lyEkta5fKa1Daa`d!9ECYN&>&!|?)7ss#tK%`X&ipko zM%`l$JOQErS7m$ky}~uDGtVy(V45o4x&tlc=!Gtb*ocXgY4f}rR4ipnku}M>J0f#T z(!Vfe?krq)0+2)C9uODz1hoRcm2tN~X5&S_zW4--MS zLZa$3>r`DCMb+twA+}yjsru@Y=!4{r^HYFOby>BZWl~jFC`!$WL?w+9d@L8>G(pDv zHo{FS_Zl&^N#&pjFnLdFJg5W{%n{#?gRs5Ji<;m1Mqkt?wV<5IWl!D42B#O+%`)PuJSNLf^nP2Z|KjI0%&gJLTdt#NRJL3vn z;OCj{Oa`R}3Jcvii}0K@AHwPdbksx?a-Jfq%0o6L=dVl7+ZcgDAZJ-#!Bwb+j#^%$ zS(Fx6LyeXrk{T^N%k#|Nv-(UVWc>5njR9pFKC0b#%-V=xS$5dOkD9wDegwM|>C3ej z{p5l6LV$on<<^MSop(wI5SeG>XMDuojUJs-k6kl2*9JDq7uzq+i3F zR7P+RL!@6hTbhJKHLPF{U{*NPdA0ph*?4#*@N`~RmsMRE zQ=M1dA(}CNJ4;0fI%NwvnvrY)r{) z?Od^iKxVQ$nJH9*%sj?eoEBLlV{%MGy?@r_nO=*CI$k_Zp5HKDd;=7`*cc05sHaUv zH{r{)B&edg(~8li035g&Z6;(^8>$~)U#=A+jo#+6s^7#^=Vl#c-!*zd(CEqXvae7J zZ1gNji>sm0%MmW~X_-Fm?5xcb1zz4K6*glm^`_;0fNGDt4`S)vxQnF-)|R#^pN4JycoMV z2PfzH7%*L_Xk$y?8tO82Hc#BTZ{6>y#_5YU07WbHJ>XPBH;eFu1V^UQ$z{}V4#6JD z!GlgHZ793C5vEQktt%<5q$DCCbPhqbCnm9s6G~RIB{|-Ynzz4E#6e zBOCZzek>nccKR(a(}`1kU07GRgWu=;UgkrBv&S?NIDMtUTl|*cJN;ArH5Q$mTtCK_ zOqzmkyU1r&{-nA3AEV3J-1ZKS|3BW|13a$c`u|>bW$msk*_PE@D5luCF~OJ`Nd#hQ z5<=(@I@naxE_ms8*O=Z3odBVk8ajl~Lhl_yZw5jLfdoiTNZb5B-!r$&t|ceDzvp@N zXlLf0Gjrz5nKQjD$6fjXU7xnXspdSp#54?732Hv9!}j)mYI=S#y&yFDA}lvn-IOSc z(eWA%O(OBd=meYr+hf|o<@p|_H!Cs}?9E^)U;PmuD%k5O6Ze81Z<2tZV9W9}MxmMu zwl@sb%!f@S8g(PvRK_zzT*j4`j4zKr-;FhMoL=(EyBPDz`?%aVkX79kQC5tjqfkwizKgtpkM+d|Dr>y1sB!6O@Ko6XLEGu`{-=y-0lC3MfRh{>B-nZcOO6coYZ<`#I&KlZzLxqrW)Z%W z@s$rs%pZ2Paz0`eu~+WoKBsUixb7Um`8@8IDn?35xHX_}>XIg2c&aH!{HrHizQl7D zNrp)?+(~N74$B8lQhy&p>CAM7DFxerNZ+iyhQQkErun&U+{vHH=bONO%})`;^U})W zLa?nU_;YXto7el3>DzvInLMZo#wq!Hz4b;-kZnf*I!{8iR<>7Yjhn9HISC;7Xiqcu zGIO?>dz-lrX0Qh0(jEGpR;T>g;@BSqI(+~ZI<2=GE7uc`?#2;Cx+@1_)kGKf9Jxgu zcSUZ+f%_o26%+1*#xC2rPcf_b2Lg;)X zGrXQ+AMONDMRRBDY+_L&qG%o}s~TdeXl|gQc{o|aiw%G%nzHIW3^Lu#>wG~6LuBkxXW)G;y3nL6ir;`F6* zpMw9Z<@;fv$oIKevlEa#EAmBk5II|?8@8f%$&xq+`>9pHL)??^tv zV$v>k>Aup&lQ@!v?khc7cJ**f-B!B(C4{Ii=}qw{7ymcU1Sg8S$yyRL;@^k5tt zr@H6?*`{jZVYt*EjNOR?Be~c4oy||@a#q9sI;}re3cnP%>MGSwg3Emau6y`-^_Ilr z=~Ped4)FgrojL{->zeNqkkd1KG?!OS)1=U>ymI;ls5PR!rbQ!zy#bVjc{indc3u5w-y)eQF`Y`8B**9cQM)Ca}k9>2c1r>Xdv=A1|{ zM~*SAJ}Ng5L%1ahLp~^)rHIZDkEIAyZ9S4HO%?NtR4<7|>UEOr>amzouWw7e)THJk zA*5ciJ@rz!CiPmNOo1zL6DHcBkA~-YEJBNjBY+Ls;bAzhi(sfk)?O8w{C&<(i(<3M zPhStTuBy64>rtK_C>^ODWOIS8{c8D&_644T7q9)gLzS-{Q=|P7s94KSxH{Zk^&BIx;s;6P9dH9ZU*Ue)H zLGvKX%Up$O>bZHtb)#v-nuqHdCYi@b#;osuo{rbWMcRj*R=J0?%sU%c+V@|w*Ik>Y zInjS3=c38^T)ZIX^RbZg2FRJgp{Cr6!DeWFKKdb;W)a_eH^MX#^Eij_*6w!KF2v0U z>PJ`<7REMog|9FPiwY-cFvG3aqNKgT8#9^)cojQqb;S&$g^OiX&%@McVMB>^6FAC8 zLNHp8Rqy$DblmDH6r^5*l-4)|*LHs~V^<{6+Rz!ih)+hr;%xzpK}}9|9s6cnY2Wfe zrzW>p7o(fYV4Gg;ZaU0);v{MlH6bVA$VRBfEoJ-?QFiQ7#=k3eFEfHdsEo_@4*D6t zMR#)&O^D9<4K9;%aIiRVqf~}pgghenJ_q>1Z7^74w`4?0n`H_^w}xuKcrsp84Zv>N zNO+j^*nV0MXv5;k{C>qxeNVqV&{*^teqKGGtVz%9>wTtgfiIgrT?z_)x*Q9A()388 zNb?h(ZCO3okj8ON%R@;Qkhe^Pcct=5gwGS-n0D4KcBa?5aJC1(m7A!oc^&U6d0PYb z5_CjGB;1HB2;zclbPym4t}F^}(yvn}NF4+z z-RB^mry_~M-vSG?^?JB!((ZIVIIaY2a9MK-9dNT#o42IP9hbDS zlnePw?iw1I8^gva#kCeEjX;d=>#O{T^Nwj2J@&mbSBZQ z>zq}2gt};Qz9tWo3pM$!w?3-6_Fx2BIRr!fTD{4=IcaS-J-QxW=+TW>=#j=*mB$F8 zF^VE3t8c=pDLxJYVK&!MTdm1PPuC1ojVr68c=Hu?aUrPj9~T#g!a#T!M(q}(CNq3i zV}rv1N{5cX&U8qup+mRIs@{Mp9okqrk3QjmYp}m~9w17$e331BX@!r-j|aaE&z>wfQQ*{7F&AB4>?Ty)vzYqH2D%rjX3iX$mJym>D z_;2R-GkzKeG|y91ft}3H(*d!FMW!Ov^bEx8tgrLhH%gYyqf|TV8T?Wf9afbyc5lr}OSkszG8GE)p!PNwx^o6=39q$YVBLo~%6ZcdJbRVpGYTs0i7 zaA{pZ$yX2UY1!3>F|`uDh4SzN%R?blxMbIR`jZ+};aCZu3o#Wg!PT7`&=e(eny0EI z=9+mscc?i`ZED$?U%w_Y@o)+r+81ak&kffPr$;g&ee>1Quohkzu8%vl&MVQpUE>n2 z$7?L@Zoac(?+eGWv~p@s4O_A-IZXIBs7r=P%c+9#r@Hx3a^od_UU`&EJlz?Udjfx5 zraRAoLU*3WLU+{V&R3t~!&S;0z`-4ErSkKeWnGv1x7b-@rtp-X7i3kR#Z>v(QnGjJ z_Cip8WYv4FNZt}vD9X>LkW!ah@NgS6$1g5M*+^1c=8;KhJ(Xd~!BHTRe#fZXiRY!| zMZ6S(qYX{pz@@p{()=PQ()==3n&y}INYX6#qSCDJr1=$D)t_T3&0A$@7J@X(^3tqO z&1v4GDa}UNNRsAb{WNEoa&X*mX%;Vq;P}Qg_gI==1x1?Qz)I8nIv+`zj{Euw9wvFQ3&EgY6)tPMXFoQ)% zUjeGB&UBceVE|#tFnYB64ZS@N!=pBmWYYfpt8KBx<1vF0?)C3FtPUAxa(*8aa{eV& zs#_oMkqB7sMYX{SkAOdvRs98~1iWpgTS6dUSza5gPzcy>gM%%Rt6zs0Wh04h*`P_i z7xjWNOgSDiC298R5b=_ZlhmiZ9NijkY5o-`()=+NX>RD#{u-D|dUQPvS=^_Uv)$$) ziqVg)4V1q{^=TCaecDfCRX@U1pLRQmWeM2eogYB-X=QnRT7?>-PrDuI=I!WaejX4w z9~mpFEkY@c7nJfhP>-2GDYq9hXP8n7A*GbA#qxSXv#k5Whi7?5a`ekc`SXQX{zb;JkY9@Bb%$nI zcaIOx@^vWUmtpyL3$gq`#9+ppy9l3sZeva$t*JNR`=)01?TIaXYm#W_{|C&p*i z#(9{Q;54=C{u~Z-Te)TUd3B%4d?7eJ=}Wmj^Ws())j?4gEw-xH#^?A*+IYDa)qhlY z>Xtm-csz^`)h#<_ZM+cFEwa4+qe4-)G>j9DrRDJ3c%y72Y2(jO`jb{6!<2(FEm1?X z@#3WroYk1-B`nPam?6!rSZTjz1RqJ7{i&3%@_SUbHX1~}+JTu!OzuTVOcaq=rv+@sl*D$H#J;eK zMhGM(%afQwHA#%7MX{G{ROu!ql(>9l3iH>sF*u8JPnilJGWL$t+?^Ul>yKNjxVF-v zCdPQ8>oZp2tHu;vpIyY1uB_yZC=lxU$oASrmO7hXl`k8SE(jnY!W<@$(VteBG_vE5iE~X+i;}* z=#!E!NNK{V(gGhaR5U7Xs)v@8S!H1HW2O=x9xh>P4yj-lR@>mT=6CJl_e4g-=XoN9 zY1ocnVl{EH9l?riQf+ydq}(Qbxj_W}4&3yjgIZ@w{xt8gJg@Qy(mppcr>+7R<#>bnmRKp+d`nJvOG;ys3uJfk7}yVjIxoWY@g%T zXBnm(oI6~7CSG{2-qmMG+1}7Dp=1Jf99X-0QtLdc7Sjg6s8?8ZnUEwLQMB4A^AoBJo) zk;eYxM`;n8CuCN<{F1Z6kB54OCoVQtTj>P_2_e{$2LT7RwJJy7bN{?hjK5ZlHS1&x z8Ei#}U~3Fc6&ufCr>dNC|7Rp;Y&jUKFLGVvI@_%JCV*<`jO?S3iO*Nn@ z#F*2_th3!JGh>v*c9Gb@qUxz-@WuY@4y>`|dhb*1%sx6Yh+0`2cH_6R*fqwjzjkWE zz?90Ask5`OzVa|*;(o^3yP@om*BY7X=A*Ea`buzaVX%8u_2#k<{~i8xx#1syD=o>N zgg%N>h2LK@%0{xe?~-8_(RUFqY2QWbAbTLMX_n?Gph)vnEYf@A*72;0^AeNV1{R*ZIK1-E(~_J1XobEeKjD@1x90wJp-Su%)dj~>xnl$h{3M~rO^FNdkY ziG*<#a3h}9X+C(iyfep0^;ve6=w<$NThp_`l9N#nuOQ^yzbuq9A1i0vcvQ%2JnA^B zx8`=n!-c46uoqI`*o^eT-DqL5G9l5?Z0PQJm||RgrgL{R3Aw%bt%5w8=kCq|b{RiU z_ml@-eK_9Ea(~@)WHnIe$k(yZk*lDYEKu5tZKTCmUj(V)zvXI%R5_2 z^c#7JgkY9embZ4PP>`~Zw;+I&)lr(G^Qv&gqG0{_(>$ZkziL+F(hWR5DBb}S@FziAA(=a!&8~Hk? zF>fWBLU4(EY8={|ql(B4THbyG6nVQA7I}LrDM_-@@^%LB1d!$J+CY=MH6M{6mCF%~ zpMw+WIeimExHnl6UP|=J!ppF;c~iwt7Oo?!x+bQwa35vihvFp?f-IC(?^!dc)fEa- zmqSXM!UVT0thgMyLbbY|g@H&fc{&?P0ZGxl~l_(rPj|lTYqlI zJWH0PCs}|_Mra6#meB8v1rkOI*sW zPS%;Y3`f?PP~z2@*jRS;TbSxh>?f^T*F+$MIuo+%Jr^X?^$JI4;&Ouvqxm9AI!Kd0yKZS3Z(CsO?YmNVYV{R`PL2q7~|qm{O#TiKx>f`M#{` zW|-=c93X+crzj#J=#j|sR6?O3Wh%i6j#Q#!!G$hf$B7sFPlt|O`89m(Grw%?)^V_l zUyP`wh2aA4x@1$G8KxXu34(UPpWj{znd|NjFJB~)LU5JyJ8}Ai+@0XpwS3tI6#23} z7WwiO;5<3UNBD@!_zdGe{SFrhs56b8r|=Dc{`cE_SH@E++83+^y<+rmH~FbyYj^vC zuUQN`5JTULuL5#a_}1&A4)GSEn3Sp%S5J5JFPD1Ua*-ao4i`7i)tUP- z_{c|!n*q&^SmCvJ3@=13Hk)!0j;x}QWV9b)XW7+lF}1zwKq*yZ5)eYODYChADye7` zj_p-Hf|&Lr2p%7v?&c~NiB8;fhg*!2ge=4D@o0|zYjyA$Mf^@ zTg-a--IZg%T5f&I?_EHV-?OmDZwoP)i8+;31MeD_yW%C3Bza51yp+N%VY#+D zaF=UaHDre1afUgu^Wu*v_af}wL~BvP7F!bb9@(0;I!jCxwrBE zZOe0WZB-$i2kW= zM)cubG-6|0!VlR!J#Zi~YuKeA;i)5OnAYarS8IEzyuBhJhSm0lva7Q()%G5wTq+sc zLa6PPUGI4`pR%{3iXxY5TLO#C|Gc)l%W(ph3JdXGu^qCZ>mKvmV!rU<$62%Y1VUn z+y?uc@&?I^DkqmQr6-~m9+t{~1XdR!o+#^6Flk8%& z-nzJ^9Il4P$@7E^uff^j9Z)ITnTT;+E&bBFGPm_TN!C`DqXrLOrqGw&x!Yb1y(+IM zn|1mZN}O}X)LCFL7Dj0{>#C8)Y?eH9)yUDZt5wXgTs2b7vRMe`Z>qArY*sjkSvGUk zNOrH3jh&6EQDgYkkj)v!@28Tyoc5s&@Vl0u?mbwAi3%*+7d*s$MPfD0|B2HlvR9um zrseT5pvdFnvDkaWv~%tNe!WmTpI^Ru90<#;VljHPTRn`IF)I%s!P+bK{Q~Av9Ff!) zXJS`Hh&2iET5?D|*B@hNV`#-k&-FxE)nhT$bDb*@syoE#RY3GyWz~CTBx7iWg4Fep z(%vJ%b#(6pz#Ov0w39#189oSagXfb9y3@ji4*>|CM;!)^5;S_P8{iGl3V*Y7+!gvQ z4%Zb@eQHe7I-ex^j!RnSBgN&fp}?tJ5Ne%e*L!}R+%s)#@}exnrl>Duh>Qmso37T{J`PI3s z+rV#QdUY}=^y&;O>JIJI<&ii(7Op!^OQC+y94)b=nxc*eykg6grqk&hRk8 zAqck|-cN3q!~2xyZ}Ohk;4TK$x6|F=bwM{w{W`sNei;U$AJX%f!W()KObbX3O7)_n zR}RVjDAXknLl{29uj7V^B(WJD1yfW*FRB{4Map(G0Cz@IEyzie5{=XBpv0vXqzf-C91-H4p-Yn12)X_}G^9sq4v*%=0pTgAr{l_ZzFBY*#2=4Ef zRquH&>1HYvq*hipll%Jx=jp+v6{1v`w4O$(iaz{QFG6d4S?L29GbZ-B>nzg9QTc*b z5B*3`<3*g{k~idb zGWiKV-1{CYqFzJ2?@`LBrr+5MpSuJJafHOtQ*&|8MiRlL>mZJMq%=PlLpo8Kzqb*oJ&Aqu&{%)g}KGv~3gtHZwaR)^}RnQ2l)vhPOJ5e)3kw*I=YAeV=0$ zzUcQwFDZ^?@`ai?pbPBf1ZEBgdnJLH!@*unVCHbJ*AkdH9PIT3W)25?BY~O2!QM<@ z=5VmL5|}w0?Ck_*4hMTDftka>79=opIM}-h%p4B(UIH_RgT0@?%;8|aNMPmwru)#< z|C&W!=w915?B3bnJiR6My19F5uVJ@F`f_*B9!GKJURwczAL3DUoazwd{y{?D92Tdm zWx)MXIP4E)Z=Igu7C7HhGbrzA=L-k_NwwTg%3X9Uy-p_B7KmOY54Mvd7?1K#h5xeM z7f)2Vd|`$Sf2J0aw~H)HAymkCl3QyMjzS?69S64OOyYWQ>D+!$+0}IOSMWyL{|Q#y zr#zkT<7rRk^Wzi(2-gp2dpo(rDPR2gY}>el;>iV*ZTWqCt*g@Tj~;puVO5S||AZvf{b znFE@0EJh=Gh-JByJJfO8I$C9Rp~z9ZBwi?UUI1|u#*yJPVw zcdhA>!)vAHF|jdE$jakl0y+~a-=gcP{e!FJUZ$Sf)6(#FyrAp< z#2VQ;eq9X~^OZdyQRCt$Sd&?-Ng~e8(GT1;ahL%9Er2DY@&s|!Mx+7rQ5{@1DOR&I z=A-`z`lS4};y2f7R}Q{N)X!0D=8)fCVc^<-HwH7}@)ABJ&jnoF^vz|5E0Gwxvv6=f z`K!zn&x?U!v%8t%yr%4ih9I?~fn}P8Oesdi9(m8mtq0Ay$8{sLt8oQgrZEcZsm=^&fm_J^2)s!ac-dBn0cpvb@XV6be$hJWlJ$%-FE7myg6J zb&GVFo`~^YjLJP?*f=sJUlV5Leu0-p6__m*cm{%#=Ls1;E9vDSn2%(J&+)xfhbBnt zy*l9>w$W2@bnC6N6j)a`#SvvCW*sHn*8CdhQZ&T2v@@4dRxB>@R6(aI@w3TL-rxbD zDoD0Bmtv`JoJ;YU961b=yMAdVRZlL0$w#fi{t`?Uqf(Cq+NeFg%v2;Q+rwxg+1h6cF4o%Q-AR_!)YBx{(Z;k8T1}N*@7XA+ z+)c8@;AMzS(T%l+$aKe(vd_dTc&|)F``ghvD@ff=bGnV+emB0eM6Zmm%JSwlQkgG> zuM(JG!p7xR9qPfa|BKC=T7FGes#T`rp0X8h^_1Tsq(1UStf2)l5m?e`L0%)0bdtlv zl!Dj6WOrX2PlTUP?n3p+a3_Bn51$R}a(-UBqVie@-Y^n24%C9uY^x(j!WHAdodIy} zz*roGS-FPtFp{1AxUIQaoW^pDK29fd!Z#tM-pgCq*~n0|=)H8xs??6iPWrDK+q1Cv`0$^+e&}gR|UJFMJSH)O#77vSZ@8UI;oQG)% z-UDN8A!8eQLAhJ*OvirWu@JoPJZSxA97?yZ$z?1k+da5lvpkYHm0o?FAwhkBUsa~R#LfgI;#8)6vZ`H}s!Y$6ptQoxyD5OE zOl8%3HcbSjP>}i%QmRY^x6L_Pone#16L4aB7aYw(jl00XV=P=6ykj~Xi_=@(GClyREAz$cx+YuS#*P*-%OCq7FmWWNE z$_(|G9>tS}2zDTC#i$eLQe!Av4Xu}a)msnG8U_m{YGK-NNzH5xARSFeE~wcQS=FU5 zrDkVI&HjYE^N|p!nJll>QYc9I7Z9QBl7~yR$8+qoRBZ$M;jhq58%%s8eP3I#r~x%b zXz;%xB90RV6K9K)*~W}{8}0x!^4jig?|Z|5jUdD8=k{-_JX7f z?qNm*=Ynb2&!_p}fAI6_Ny&(FD)O22P0_=t;yVh={3@oWhb!m)Lp^+;>EZIA(8Cq5 z7)w$G+q*ovGx>CM^{?tGRH{Dqum`+4WB%7Bp!%xtIf$u>eElMe04MmA! z(NL7+tKM46ijoCW8nU8fDx9sSNrf#DsPNZ~14{)ee6CW#75o$?A*8~xz0zdKXmO<} zhP$HgT8mM|Xmk%zYsQn!*@O)bgf%-I=zphsL@RGuHJ&m37tPdO%CxH6kEf|xD|hM> zI;Ka|`k@<(o|*dd#PmjJC7XpoXr^9vz2~3FWUVpHemqUqGEpzMZSH>;#bOip+psWb z%il>YmNM=or?NTSr`l>z)^lYx(H!Z^)0*tChxHBZ79V3?eM855u{NMEPH*TNo-gKq zV3mdt>Kn@T&U+a1MRz`zuA%Pn2~MPPqSv(!;nIGZc!~RIa=IObjfW771&f-#+3rXq zty(-xDPY9x_01+yADl`=*I+mI%^m>u96ztlkt!7e2CROa^8tITfk$g&@Gys&2KYwi zTr+d7g)`VTZX1&^^;wm9XcBi6Dj;_hib}opC@KDLC_9zkVyIuVA)ASf)C|nDn8W=oxy2psZ_BMXx;si- zQ+SLR`Ha|VioFymbodf!7*Kbw7>()?RQ^lGA3=^)kk1Jc{tly~f83BB!`};84*!7F z)D~&=N{P{KrtiqEu8XPNOh1%b?FK!zf&x-OmhII?3de3Hzb$HLhK#P!qZ#@Wai_ZC zVMYXh217qy3c#QG>s`osX?~tgh^s>I7xT({&L@)Yc^d*t=SH}<<3{?UQ+o@{O_7P} zyS@mGLoC0(hgb58dpDF{%Mu5>#L{Y}wmEP%cAB{EI=z32N$yRJ}>`T|nw$qH^g z+4(R)Pxhzq)3D8aOW+#v^dIFs+<}L=--SprH6Y>CTThaHT*pez!XF=Ahf?Ly)u?5R6|tjSB~@$rbAk8a@QHz}}mV+$-RY zFn!w=@95hOSm>J@N^Z|b@Gzb^xG<&%CDLZ|rN)#hpIBb(2s-?mny&fOeA25curs|9 zN%U$bS=H?@rB@e8uXZ6?wrK)FuVmGG{++a23I(YvA!YZG3gN~%wr$sl8Z{OQ8^$>q z#vkX5%AI#*I?fRj={RRv5}GWW4q1 zav?A(YWb+QUL<91Lzych-P)vN{uqg`n#^~{6Efcu3z=^MoKE^)eCdI;+1Tt;#0<|N zbHh6)x#9X<*qMw)f_~XPvZ{Mvs$a&HxXCiU5cJDr)qD0%_KheM{jz%?rQy2Z{$i{V zHEPJ%dgI;7Fb=C`7*2}2E_VFZ?5N!F=cIBLBWWLeBCjGpO8AxJ@)E32i%75$HEPWX&M?VoS=J40-lZXr#fX>Y z(Y8>=- zw5)1`DKT6sFb9^dhb_9 zabTM&xy`oDQVIQwP($u$HeQSgItq55VlrO!FeBWBiizxAd^gn>aqr!yxVXKSeU!=W z7*NRWI4r&6rg(FXYx1RuT-{2q~H?N(BlRZLa z${oI|TiT-&aTcScdH@6^WDMn#QV=}_7wlv}F0gz4{Tk)T(C-b1TdMdU$f_QTsVe?* z<%v5hBm`BwEN?ueP)OIGo#256fPp4!1~0MfJe6+;HC{Ou?U;2iZId0E+pC7=*0$M+ zL>>=FBF0?Gr5_-nrG`WOuA2sz5}CETR%L7 zoW6@+^O)onU?1@F`VPvCw5`**#%IAFYdUrYD74`$EXt?$*E^*MevIsx2t6BwrzRa` z%a?%_VWJq^h@ZjbOkU^U&vl*lq3e{KFDbdsQuw>&)L}a}_47Uaj6aRY?1<}mz_m>v zI^P`Ql29*kWNYC4jQl2q%183;yz+V-I%33`G>|HD{2@cApb>;b zpb^9mWmnI{)Cgj}H1`^dTnLRIWP7TsaEu^KbrZOY@(D$`aYGlfF^vIJI&I-+NTpx3 zY0T+HgfO6|9mbq7)vO&E#%YH?200zk{)nHa14@^t17qyX>Jv-{E&zoNT#AJbJZlVH z#7A(obf6SG1G59aFz}B6_XVd;=4c{MUC57$>A*KYmcs>{LmE_9HXmIgl!@UQPYfly zn1ieiz6;dTjUp9}NIRPO=rVJQ2+uT#R9f#0k$+-e`8Qv7^+HVL-xbQgU5ptalz+0l z{8PB5{PP5FRE=7*;4@58EE~pu1?1k@D0}hb$-XD2H;qm<*|+1rM(=qf&( zkSTyy^WlW7O+O_k%u1#ltk?>y-{}cN=)r9y;Aw( zmPdrptd(ppe-y6ighDHp zXEjz+eX9woJNW6$adUk;6Ij@o-%6vG-{W&{fIr>x`zBE2_fN3M?;FK_-x_Z6?iOHa zw(G<4kleaeZp-J(yuy@^ZWDZa${!`F9Ot#_6sII^!#M>@-W(A&yDG>nX=X|Pvw2Zf zhT>0!=}y_zn=w_Gu2N=cV?OT!08(L+?PZq2H5I1*%SoM!zH$IPk>EKU!PVv=C62cm z%oKOpICO`7DXB4+Hx0w3TJJ@0d4NUwzjGOj_fjP5)xD-D7K_QCSy3{~!gcS3i0N8> zUfnA(ruvYK9nZ=~_gZT2GV^XTe`e-An8D4YU*q+}KXZup1CIll(}gsr3x1ykH$Vf8 zX_Y%PK79amI=&q&g#V^bJEb@1;9|3>x&QV(ar&g(p)7)&WqSMwq|oEXu+%rzR^irh zYy6$L(0t`q;;KDutjH|ZTsj@(aUeCvtphwEfVgGHu$fN^-~?g8xyK&JT*k_6!5s*t zrevc%CsWO)*>7dMfNb4t zKX=wMSp6HTxVH{GAhT}`wW0aSPn4b)V`AC7-uG_awO)fU<8Zqaw_Of}A z87XJ)o)ti|5Z%II}9 z4IPQ+uv_lFuor9Yg8-5VJZW0J^-hJUqhyok3F$~Rr!c$*j@@Y(wH=M8VJwNX9j_|5 zOCxQ^wMygX%9}_CwH>m((=e9E5%FmlmOSh?R z_jCw9AH9h?u6L}C>ENK}U8^Kz8+YOq~?`v5L$tMpp>!ZjOEj4bk5JOBY{aRM_ zJxon4T_@@7XwnmcsU=yS@DvJCHWp-R$&Oi0z`s2KZX}IX(^=*W5$ks%yO)2Fn;_lg z9W}9#$}Z^(bWJbjz6bdWO@<%i#fAvnsM{9&hx(Qc@W9xWjj4NfE03@nI(PvCA?q^0 zV%S1eKKHOL`BM0BIVv?yj1VbhL37jJC66vP`o9Ga$v#(OR#SM~ zpUbTn?e(Q`r_pz|;r;IbS#|k)IfA(ql#UWv`A0s>u0y@`NNmM=(LVa7=USpzg5c`x# z?(}G-H&QgRMy`3Jm|>FoOO*_NS|{v4zPi`Pt9QiGLV3+5%&#g({{k`}{S~t}%g)J` zk8}39U@4+VmVaLWhrqwE$iIDvyYgt9fByhh6HW5(pK^;v$tamd z*~1d5`i8 z7_Fv+j)Yw)V%!)PXn|dDn5-IPnB*Cd^i9io5O|Q^3?#cWHs=R$IsMRjy~dWFekhIJ zy@%0aB#i!il$V{I&k>mOIR?$f51bol0THzd8x=6afc3l>I)d-uKB~oZd@ynP_#BtU z=UC9=&DY)WfqHwjK*1$((3 zSXd0*u8qPQdZO9Ke!a-YQ*0vLVWFKVqBZf)u2wS@Cd@0elH#B)RbL@hro8qLD9^zAByP?gi{}nlYP8*HR*aSvr@EfSbemQ_3;b0AQ+5R4T{h{q z+m3ENOx{X(wiQXe_{IiDlBE|vT2{4$sb2g|(&C>>%8?NC;$?XajzTRoZ=SK`ayI)G zo0X?wj~%zWi)?o+T@$0tr<_o4o=*`j?ueSVq$8>iy#OiAbjojOFy>MI7BXc4(}K22 z=wVud5n!e!b1J*x<0LZsGk(o|vUc=t0zXgpC7D9dDhg_!M(3U;JS)TK<`_`u=2$Fr zb1UG*;C@t@jOqgBma0dSOWpdq8PSGC=qM{n*x3rI2$2;%vZ|e!%8FZ*6}v!`&9{Ka z3R(4@9g<}wg@RNWQnpGhgp-Y}QX5gDMt84al{&-3%|p7jcIt&`-$`sN)Fx2odx5j= z{3>}muTSozZ*WNEC19*O%SXNSW$C29?!0m#Y=hOXq{2?Jcw^A~l3_im9;GuHnqoqNFw@TzAiA3G4cI-^#3Qu=y z8Clh(FxB0y0V~Zt z*jbtthBWues`g~iq{f@&;C>Q~s8J({6Y!KKF~gLD zi6F?-q@Q`{acLUGM& z5H5v}fp?UKa%~cBM$=1U^}At`K_UUJ*o+M*D919m!y?)eE{n^W_~me9O}t`O6F*sY zbqc1M_@7Et8q%>}8IYQI+4Y`TiGC;?O?+UMgKGm)_*DP{U4+Cv757z22U~wO6ETz( z5HmTl+y{z{`}Eo0T#LzO>P&s<^r3=zBUoe8*kf?CJbu#Z&%?9^Ujx&)S4`ueCHQ&u zoDx|GRxq#W+#h#VPBR@0xlWb*n>c;B+>wlBZ?ODX5fo<|tX_{+;=^qK?)Wr&PJU&e z?tm*-4&x5n7(v!n#YM6WFj9ckJV3hut2=-^221N%i?8F#wN8m!YqrVq-B-C#Cn_|h zrgIb`f})Nk{J^CMwlwfvOgDl3EpHyBXoCXzc5^7qbQ>q56|hKOld^-;OkY+ZyuMtqL!IYOZl|P5K&3Mb1F zsxMl?6|KUUR+)}HZzX*s{Q>P?n^sv_@Kt>Hlmh1q?nKe3Po`B?6Ml6czs%^xg0Bkz zKhr8)QPDQ4W@ zN8_`1YHpA8Zh3Oz_+qxe+s*TXzZV3z0js_?RXoh4Fm?3UD4j zy?5W+?l!md)Qs<9bTt0he@!Ie)i`T2a52<5J_D}}q_&R0k&xlNh7urL7h}dgVu$;j z1y~QGwmya~3Q-L1qL8XU!3G$*i0WIy_ZhmceZlPt9{bKakKe|_3S1N-IXFOg-7(uEa*UH=5>0niL|fbUK5MIja$&k_R3S_S{wDd^r9vM_cL-U>(l#AzN@&Ca=4RwA19tJ z*-VwsxONZUMZZ!@{Q7I~y1BgSmT~hxplx7r+MO~x1P^vCh4FrvW3+}>nO@>enI!SYze~E7o&E`wa(j0pGi2z7dz5; zYMi0OOy%v$eX(uB`v=ZFcUuA^bA1Y2Z|#CkaUHb_ zP7<8lCG<1XBjJa;OK2kL8R{;fwwy-$_j=)zyM#^w&c!}fuzPA2cRrda_9VXWkEElv zyEvlM3)?@70F_?i(FCn9;h*G3DauE?Vlv0tC$T=m!TDetJ(5|9gk_H>0vue1@OcNR z?W{n3M%TNA$e3`XHFdDUnfXBJqJymH(p!keE3Ix$d5pV<=*cHtkv@X*oDqrJV`Eiw z40aEZVou8~<=DQH^7=q3r?k8(CmPDD(NFFn8V4&XqqFc;lkjlwCb#5~me-~5mFla? zkg7BN<)c0D=aeYih*BME($X6uFSTmy+|E3l#=?TTM@)0+(<;kBcHUm#Q`(HFt<*l` zijcZb5JJb^kjw>6uq4c1TNYuE1dSs`DY`?wAS z5p3=v)jdNilC%erb!EEAuTVP#4~}N|J=Jve)|J3iHpU&?O}if4V;u^2w~#ycE4m66 zDn75ih|61Wb%}wpY6SK`%bQh-#7mzKXC49k%`obC1AlQhO*^y|rJ= zZ31)L+zBA~8Qjf{7V*yh1#|!~Gwi!0dQR3cIFzg)040exoXj)|h z$@64jsXR9<475pfVN!F_7fb5r=Cs*42uDAy!xLE~VTSvT!#>2&VCvl97RvaX;}{Sm zUHd|KCm1&hoO(~(7F+rk8S>^M07JfI1sO(b&dE$8SEm!Wh=-+LV00-nxsp?b8FSZ8f1< zgQ&~{g3)}jK8wI=sgSpHD~VUk#N@3QsO%`RU}u+!NgI_ao$r~59UTXZmCEQ`=jxa4 z;59L=9z;W9mnBsBAD?we;&MgvO1Nlj5C0pUeu4rOtWlv1WPSXr0LSg;H8{Z~?dKy_ zuM0n1`?)Vt9jg88%Dqc}@lh{)(theaN1CLEuxNEG?&w-^5M77qR7NGwh9r3Wp*&VL zrP8R~VBVZ*o;L&9zl}UZf`*&(&F&wsCH{$m&=M=2er*HIS9XWx+HFS4neyY>WC@7Q z{*5WJ-nxg><_)Ur;9e@L+O39?Ij}j|#&$ZKCRjGE;WSMe+{sWkn_qdTx9%x29Bya} zx|lL@!RoDhrO+d2_IcR`iXO?@)uPa6%+uVTE_9q07u&!}7LgNQ%8Ap9S18p3orZT3 z6MLZ~M!&$4(H%>{(r;@H@3H2P50t0)G0{1>Zb7KtHO~i|b|&`5lXWJv&zT})f3G57 z{gQEl`V}sRMJ(=DSafN>LP_-d751%u#coi1Jk_7HUm*=`=vNs1q+c-`uGFu%9$z)7 z2=1HYR+i$vMQ-H_?jOr7(WU*30|=eg#f|0ZcKkJ#x2*j96kJ?>?!@VppC^oe;fE_f zhf0igZ)QH%mm9HoGUj#t&nLa`N%?8)e_Gu6=q|CB^*`?wM`(($uKziQc+~$C#_76J zl#lMgr2km~ooPA!PhrbWndAPaAhkObsL$wn{ZC`Ukxu%bN*6k$=u(wASZN)$|Cteq z^~6|B^+YlIdSc&6|MQttPN|+KCmQs`=qGw|5UfZ~?#5S5!o&SDxh0QOA0m9E`jE5< zuKv9bf37Xc^x%GAu^v2#)6;`zjX&Xs(}SZWn<4a|Bgb@I?l~`fq6cqLJ`imyXiAyI zU8zB(_L$g+ocR#$ns}vJc_f9`Tj%02%kSFbnUAnYZ@Kn_04R{i_8&$}`ADF!q&+&Q zk*G%|12wzcx4fnK5uE{40Pk{IC)!<_VQaq}2T8%9_0^Y<-%{-XJfl{M4YgWDz&P?a zUaOrB)1-eONugO{C)KQZk{+7n*F_T$jDCJyWKyz}#5!h)N_9+Z_&R3aiH^OP=0U1s z%H;+fGx~{+9SKv?v4`Y1qlo2GnqAXdbL%3}pTocF8nHI9DuCAD6&fA84gk(xfQEM=;SXTT^_E7~genI2 zX(Yjo%jzHLF2l)?co~|Nvn`#4+RyREtP3W?kaHB?n$4>iPf!#r9DR^y!56|UX;1(* ze=`<(-tg9OR4>4$4~FRjDZ<7&Ig!aM_RoTho)a;47|Y{UF3OY^bG2IoqL=!8!%VAe zgFSCseI#_$Po7rUPVnu0xXsnxZ$m5aI|{#(kJnu7&H|9`X_YIG#k{xSA*D-y_agNm zr&X>NZh?<;eebUN{wares(Qwi=7& zk6{&$x<2|VgeOobg&VhGXlaAJcEk{TReOU*8~#|~XF+P5>*2@a$5>vJx!)2XAAPD| zx01fB&&~YgXFw}&+YEX(w>F4UeGXz`_#tdcNA6WV{uNM~$I8bxH-1}b0z_9wCmv{l zld`^YIqArKd`m|L6Is@FHLXed-)~r*BiL~5#%|z-ZZ~d+?weltq}|vbI2GslShO3W zP1`VWQm!o;j}h(;ptgsg)WE{_uP>jGK;fPk?Rgdx_rh`NSPp9Ao-KD5?#kY{YWrYN zmn*2(MX-1r5$ht{#LL3+*lls875TovsK~V*u0G}U*Jtxn8HKp+HjdJ|ThXRURf0dNLEtyAw*L?DTedez(3D6tsaU^8QFY^%gt@HT5gv{F(?+Co0&P6QN`L_pd z7jzV(8545ES{@M)Dw7>S6pQih3#aOzomfVB! zj>)496!K`tLLM4p6{7?zVQrhhzxWDtmh#EC^ejJLYbmvEgDF=sv zi0{iEl{@!}bl-=#C6caSxhhG&LXrMsAf@yR?x){~8a2{?ES}Qz zXP9zuoKl$Vd-2jQE;xTLtkOTm?h1d`(mxgy>F>cJ{TEpdb@OrXhHPIMxX<+ADpDM= z7TIEF`HGQ!Z@~B9c^Q5kz9*IG7_tS8@V2A07+pzFn||oEt_`azB4gwLYzg#oC-0+EPMyVTi(5q z#FO3uk@RQi7xe*;E?E+uqjBb?^cAb}(tc4gXEj0CkR8kMl1o#2KM{1=-oHow4ADGRcw;mlEe^5pAB)ixZLG=jG+l-8p%v9#SyK3rvL4?`k605w^plqY8*MR3 zXkVJ1Lo7d&!b$cUd&K)x6j+t|(%(*kp z;7O5nbJ%eoMDvRGEJLaeQ%@)3`6XVMCyMZ*fLnpgbp0C57{;g{8A@hw-15++$KmRY z-($aT9wtBu7Af?i2hbwx}G`q8X576L)b zs`q@vlDV};3I(Z$A*I?_aK9okqDGC1Fa z*|q0BBls^(b}NHIcB^6`yF*QOt4u+5D_|5Ob56^gE1GlV3@|-&u7opKL83V#7ERP3 z8byU@R+Cj-1yiDVEEA0oh(;EV-z1_@C`40@MI*Q`8Y61d5Y1uS!jg(6!<2)=L8we6 zvbpKrR5oIy5FFt=Ok_;*F8)3=*{lu<+4N(LwC#B_Og!IFIvS}c$I=(BT*hrdy|`M0F~IVHmD z&yC6b5g)%Y84ZF$M&H0ff2zQX(e7ewbrd0Xu58Xd#PgmQ#mJm{WzN}|b8no%T2i7- z$r6>X&9F1!Dn5j}rmX53m=f-jnGy+saAnndUf^C!6Rtu*YI8_Qwu1XgWJHY`N^}vP zQYFeT<={skP@+VKe)3SNL*k||_loBhrpI9Sm z)JW_h6tpz48KxW@3c{}|jy@wzuNWx=hdB=ur0L}At=(k z5muV!X_nd*&E##gMW$KdN%Qw)Rlkd=G(VfASqRcB%S*FD{kJq5Wh2>~<_xolG>aE6 z&HcIknaKE!rFmmeq7IKA~nvZr^m?*;wpC2an@T6MXHVuIBhqQWFz-Bm^gjWYv2{BqtITN+*bPB9SW@1lM*;vBP#tw!CTHN?-l6V1HO{O(+t= zaE$+RXvTG;%J7V{?VmFJe`Wl~LyLJ)-2P)|ZgmsE@Z3tn|Igeeimvhzyh--!@W`pZ zV8{A6GlM4?4BEO=;!{N;ctt*2xdq}GCTZ>EPN!E~vHd3??jz0{6$7XF!9i4lNg=rD zOBaGak>BpL!t|%T*DF#7t;tUy<}Y=QU4%y-(}0j{OxtFm~3 zncZ!1QDkS}F(2t8Je=mt8YMkr+1MzF2+vqOg&@yyh`5D7G4ORv6SUi&ut;bB`9S#a9m2r~IZ3R)#1A8-R%SoG+ED zUX&aUDCTavJMFiMEHWtrEJMz^V6p&Np`x4g9 zd?PINUf3+jM=`oKP{##U!y_cOp=XsuoQ7=|S=Ajd)v&!Fxt(C7grH%Q<;^N76b+j{ zOWSzPLL9^rlQTVMp^t0`V3Av_O+$bTkqiNHZqNSLa!1{m$Zu5csn=8aiFL2vzjAIr z^zU~jznP$r->z85k8_Q*07$vTE&se_ZO}-*=bXu&v|`kb)8^%3r*>xA!Qb#xCg+Ng zyt?Bl7qRy*YvGV>*x*I&$IjmyTAu{Vww-4-zpxt-n1)44%i;u{! z4&Dp5-R9-amMhNXcuiOZ+S$^#jqcVipFVr#k4oVxyoB2|LOy#S(m4XJOp+|t7k-5u zM2X+d5qSP(e|s8D@G&DX9lC!c_#0(45cyhho~-HEO}O5Q&4V^x235>>$7*inpx{TN&>q zu(g7tH7GF!HO1^4?OH*VwIdDxO$6&VSmY+Hf4Y>meDR0YLrPdCgh!}`J(R^ROniu!I}Ec*Segx7uY(}?_2 zejKOD{hr@}BP5*(Qfc*91`ow!G2&QEQV%XJz0<~sVr0(ddQe`6st40>Ru85@8|uOR z;4Q8Pmk=4R9@O{$5A~o$o798$k=270xT79yf*$CgOG+$O5AF|uduRF1M;0Vfvcj!& z81}?TltVo|coL4mwaEip094kfvO7q2bstPsc0X5Tw>$ZfkAzTVC)+#jsc;ZG8Dctr ztka$x3gz7B91vWgj%hv9KCgfJIGOI)3%7)&yyI>)GSs?IU8S)l*M~ChGzf6qm1+lz zsN=4b4_*>?PoObeT@Hd8v?|-{MHzP`*NZAScvm^O=DV{zU)5f3T-hPlNy_o<(79R& zody;KSg0D5qQ%^=F77q->S`a`9+Yr6*=kVWxNol=0*#pa_I&WNxGx*`LQpqWWqYG9 z27{I3X<=#ym@k=e zk+!yISp?T0UenK&hmoNk=~;vfAEWNzBnMIaZ!+YV34ZIXCp5NzV>-uh7Z8kvSqs?d zG3kn3=a|mUhNe(Dz~YU<4W|PW7R~>N|B?SGV`C*i_)efI;nv#5O1QNOxTeOMiqOQA zfgy{~v6ROzRfNWJAO{h4jY84|xE~a}w-Bnq3gL%x2Vhhh4l`tLZJME4$*`Sn!;?whAnG7vnl{p4n zDLhs_j>FL0+VR-g+?qtp+}a5Mxe$gA&8@wn>itn`I)z|vO_q0Uib6r^B%qpG({(B= z`PK-*4G)eXiq({2vEF(LpLU(<tCGnr*5nDH23vN$=Zs{+(S%Y8j)2%$8f?M4%K1*-L%<&$^lh!zco59C zQE5HKuL~{yH@{WroyLOrH-Z>A4?p=x!NN{dY&eSR5cnoVixG}7!pzSDV*de2J8PeY|wj(IwaAj)ogtg9&+F447D^nf$ z;C0DOQ%Ag&0ip{{lwI$6GufqPa_b1n5F6X!;9aBgoug`J<1g2`#2Q^HD&xC(Qv-n2 zdbM+6lrK8Lh7;Wq34cRYgZC+dFI7$sAw()VMI4LHcrekWSTP-A<#h80DON=-##*Vf zuw6EMwyZWe(g!{lf9YNt57QEy2WINp=fDtuAM*P-zn>!TJ+L*0jLzorRNF^lABCNa zodxGXGlSoY?ZcPh`!4WrHNl@J&{QhOFFZKjY^+_PP4DIBwfkyp3c>l}P;;}3=l%d+ zZPxC84;1>&K8xgjkO(>&_lNi?Mn5D_*h#S+ut@{$hG@$wNiNP(mSsSzqvilCMi&sW zJG@ZgFT&6=?2oXs_E1qXb9*tse3cCrtRv@xH>9UqnVt$k@s{PahYAI$OMz~1P6x-~N^D{6m?1ug-(EaQO_NpFe=WgicDfM6-BD%&e7#zc!ND-6X{ zhSVA>$=u8}Oq$h544>wi7T=dhv+|dbHho7UY7%7a=5lyQC-E~(IhYTE>~C1-xDB~I z$j{RiWdyI^InP?>_yb|uO;=8)`^Y-S3;_A)R6c?~(7mvFNb4CU+H@sRo>|X0O;+_3 zOs!|UCCOf4;e}v5LzX94g@Tk-Lab*8{x)DMzENoLr0#>eS1ySg%pR z<`~2SSiDgL%vGOH@;&374A@v#skUq^Pa;ZfdAY3WC77x$-%)wmoQOFx4~W`QmRFt> z3Q|^{s4Z2Vf;*|r!*?m>8lt?1?W$zehZae>HlI*&Bn(j&D3Z^NQz7U%$g20;ne@gj zl0xvlYG#Hgf>SuJCPXosIgK&B=862YTmM>POzm2o+k~_xBtc4eZX~laRZX@cyfb7O z>Ul&@Wwv~=`IE=RpxekWH_EB}mBryBT=BQ`ne-_B6Is+ri@%3y2^I&_@HYF2i1<8y zUOlNSCJ@kjU%eptW9bw0qZd`>IbmJNB#NaS&J&-=*YFQs#jx5N8VNT zIYko!9g$VA9sZfymDWue*3eFs!Vh^7tF1yz9Om17ZWigu2vv6xe!Jw9yi{-gj z5 z0KxO?A^Ww-bIE=k>`e9|g6waURlN>VvVUK)p9w+kNC1TFWz~CLONx#{L26w{N%n&K z`^k)`QDbk_4tPpi_6$=Fb_6lxeloF92zD~G)lusAby=Ek0!5l{#Ujni!XkClZlLoO zHc7SBZUe#bf6QTixZJSw+C8`w#5uSl(cq+nxwT0 zb|x)FkF34yd^)q8%GNK2t0H4{>jmf*TUPP{I78^)%@ob904 zpHLTfcW?e9=5<+;6VD52l7lx>vSf~*p$`22F@35?hfm*uMDYJ1ecW+QvE)1lizp+k3Kp+h@DgJ-b#FpcS(!5Bmr44#7j7%nv#uKCgNG7%&{?~+yhDW+z~ zK9E+Z0_D~QKr}zHyyi!tXvzHMXL5LBnQqkSJP#2bOQ)$AJ%>s}kIQ%pWAY%CjnpLt zd(L|6;X6`)-KqMktEDFYl)hiW#@ur(tah+WnR^!Qdh2X?Jr3zio+o7RA>K3;cv_)G zgm*zCxPxTdzACD(_?njX1qVT-7(F9zOvZy4oCL&a?OC4IcE-x;nlZ;F3viDSuoyjy z*;Qn30Jr|io$P)pX;@&=G%@n1_pC`?<&k0H-B14BCoOJY#Lv?KWrbIFj>+|b?=u~^ z8x%TlFBUp*AxYq32tEchE$y6Efev=?-Q8RH7xHRf@OXw)addlvEL^DX#GBNV!;>qc z4h|^=$~I%D|I&CDy|_Vv?vqu$2U804D=E-k7NZa-kSuS;NTDEQn}O)RXvV0!FXq&W zo9*p2+C~&@ctORjTQm`LhA9Qydf?T?Z+w)F55!F&_<2%}M&|A%^f;5@>j04J2Q>ccVDGfCVVL2WR**3{|7|VrSK;LQ;+1C(qT__)s z!)6Wp+7%tj@Pw@DqnJ{LUrRW@u{ec58Dx3K7ZeIoc6 zwuoeFQ&KY+Dn+!Z&r%{g5hMH9BQ&-&>m+js9VW}eQy z_dD-%&pG!z&pmfzv1tvh8pJ>Gw{`D6kz)vm_!7f7y1(X4_#f=3lA1sMQp+O7WpU6&dG`xvF!uJ~x^V#!hf4Twj`wxm65ZV2QxI8aJ zuMkw&c4vG1rSXE|J58);Kiq$)Ej(gh*<0rJoMI`70Ypl=` zQl{?tE|Gr+bUtT(L;n2Pv+uur>mWAvG!SIR0!x4_fbF#R1MHt-c`l~e*(2&hG;aJG z8)pyeFW;8qerdMxe&|n?h3)vQetv(tjGxe-t{_2wYKP?&8oA;#A3-O-`AAv;|6tkC zC$6$2A`^dEpLoLhgeOAr={YPypJ2(C8Z#6}wJZvKBAxjMjuqJQACIi^N%PsTc@417 z`v0BW_g^YDYJeZu*jeEpArt<8KgSAEwhh867N$)0kCj4^hpml)t%E_a{Kr@} zRKsb8ua%H8OU?#CRbxR|JxwNb>Rctia^b+UC=0~pVbylfe36$Zt674wQz zvD{S96Z#1Q1op8o#j=u8*6J(|OBK_F#S~R`1Ho8mFIWqB7!V8$ifF~WB2_F$7NI`S z9_R;z0Zc22z$M@{poyZi1lV=ZKvpdLV==6s{|<}+76ZEhjQa(qr)tR80&oNZzQwSF zwUVMw6}ADy@%DNAQrsHYF;+=yVJ%VIn%I^tgElCKwW**7s$v^e5-Ouls-y2!L7%H8XrgXf zLJi!dS`#&_1$WYg6V=6)wEBWU{uYkF-7!ssj``)TXZ-Bx-~+9)dQ`2KwNhj0;;re8 zrg?v+hIg84=|6OiRz|hd^XsQ(dC$rAH?NRxT={eQ!Hjm*56ynM z?@i~mpYCj}-+w~GkDU+SDJz^!|Mwu8QHTNjUBg12c6sCQ6^zn_P5kx2`i9+_lfab%Kht0R-PIv<&Iy7!Su2Zvx;z>!HUgN{r(9DHO_ zDxf~)$fRY)7E_i3O&VKFY2DOf%DR>oQ~Fw3OsU_+V#;DWiz&OiTTCfGz+#HlApGuQ zG39K4#gz769!|YgCTnV6ovf)B>Sj&71gzD|n!2i0)>M<8SyQd;0iUd?_T#gr4v5T} zT47Gs)UC5`O*^yp*0dYzZ%y00@zylQO}C~kPP{ek#Nk`h%+qd7>zsaTT34XTty|L$ zyt*}Q>$_Xi{<-Kiz4fr1>4ySxrr#WqGkxdWoawuO&I@v;pIM$WeSB=r^nrVTb2-zS z<>X9n|2SuQ*8804OTy1Zb)I=H%5~PcD9<<``CL@!p>t8f#dA@gvd=}OKRFk52I%qf zT+|Vj^HJWF&qpmw)tlLCMc%Ae>+)tb+L<@2O=8}x27B^mZQ7qVEA43Btg9#UW(_-& zH>>ZxyjiP(4M4B#yjeS^>qP6%)``9lqZ6&OPA9rPuywso^s+-b(bZFQqFWx&PW0RxI?+4t>O>!ldKZ0VZ2Q?QUUZpV@m-hMk3V;r{ZX);eOG2XTerIHY`>be zvn$uNogHs%JG-Kp?d%2ZZD)H~+s^g@rq1zrU5SZSiJP~gV%)rh zYH{=W){dJuy=mOMD$V2OHESI=Z--mlys~}c=9L2y`o+!b@JHOdvtDuYu78-jP*y2s zVb#hp3-48rS@^;dX@{7FUAn|9jC77!808YPa1Kz(FJ|GLkeG#&M#e0>zszOP(zPy& z4)1hXbbgo1qB5sk7Ja$nvZ(ugmqkv%{VbP7BeGo{N@9W$5Tb;?}+!Z~w!gCUvA&yCGoesogi@_`F8 zm-{WrTpkE`EzMm1cx&eJ!+SE9Z_&81d`|Tn%U5aNSe{{gW4XG?jpYgEH{XkNvBT^- z#=h&`F?N2hj>%)>_kH;j%X3R^9U9~1D zw%zWe*q%vAu?|3m^rYChD@n0-*OOv*-AamG{~#&$?2DvWgDCYix-sf&Y+}{dIImG( zGdW3p&7tG!Ytl}tuQ>~}IHSJCAVYnPUbgz0qfgY=^b|DKe17H?*L&96xUj`<q;U*(tt1um!mCp;LTCHJf;|sy6Y?IyUh~>)6C!tZx&)t(8sunU@o>z9K_NTHtr)ZYjxv)mLouhQi?R08TZl}T3m%F!Jf4Mv6&CA{W%Dmd$AJ_t1 zsr+hp#k#L{o9VyW?cDg)?xW3K?Y`LJ)$VO}uXeB4+&A%Rxc{E04|aRYCtL5=o)x?Q z)4bUID;LG?pSL}B|3bifckKSB$+7#t9E#mPFg$bul`+9&i|Hbadcw zqoYqFjgFq3)F9<>o2DrhLVr&=dM6~M&kcv;Bl@&DS;nK)$+^F`I@xYwtCQ`4*+7fw ztxhJ*Y;`hcd8?C}tMPkWtCMRsw>r5qq1DOhrxMfNUr0>*0;pb0OdEGIG0oymV%oxo ziD_?MCZ;WZh3P7L(uU~lNgJiRCv9`1J!#d4TuA>d^g{X{Ky&$p^wnc8r0*DiA${At z3+XOPFQi}FiupYk(q|pIkbd~wh4k|mFQj*IdwXi?RsGYxclA$?zNdfsz$^XJi)$F1 z)~Rc7I!Djo^m8D<*xYLKeSez05xu^Z;B!5@j9vP688wXUGDe!)Wz@95bZ@(iN59!+UiKyL{#VoB`=iZ*??3Dw zeE)SX{N@~df2&9E{V|=R@2g(g`S9Y)hFL>0w?0a;dGqM^Zf_pxdc1jbq2HTFy}jN% z>I)eAym=Jw|K`!YARzwDqsiOeJUV&g&7-SF-#qI4a#wb{_q(z^EAGw?uCzOQt={fz z=T^J3JGbARecN$&c24iz+0TJ{uDi2`4c(pHX~gdA>mKSkvZ3lZRsGa+?u}5-d9eWL zGWDD;vFbUITh()-wyEdL0V<`c=iE7`o-^s9dXCfZE>E_8IrHR@?Cg`*nrEM!seSgz zKlRQ&S=Zp~lV(p?wo|~I~ zFE`iyO>VAdUT&^0VC5=*ea%DudYPa6wM&5f_4!cw>&w7~QS#S~#>-!~oe1oczb<=J z{<`}``Rm^@+>jCG4uUq>jd|ml49tUrK$vV_OChNUMO(lDyno8vaHI>>s)KrG0s;SI4 zsiq>FR#Qp4s-_Zt3%@^5Q+bo6rZV}Fn#vSl>SgT;3o^AU_}%xP7#!r+cIE2!Aitgv$OnF@O@>8qX_8>!lIQlx5w8Ih_> z|BO^!0c@TdsT#92Qq?>mQnl8;NY(Bs$a6YUwQNSDs%B=SYK!_w71Qt3uN+`|z4Fw| z+G-1~)K>GoTU%{lPHnZ1b#&D#)z?*12joV&YQ9Z%)lysPs?D*~RU6z%S52j}uA17p zm+EydzEp31{iXWl`!Cfiym_hq=ew8cTZC8YX7yjGPX?v{&5d5ECmX*~cQkpW{?xc! zwJIj%s?}>=u9{n?a@7KD%TUwZA@%)Q%P=Xm74QK|5J% zg7!+?3ED#pCuq;J&Cd-uC$xW-&Rx$eor|NhbY70h(&@JV%NAzoR9cax zW3x3&$9a2}&V`Y0YDL_2sXeAoNbN{K!!4wCb@!0k6Z?nMHuDUr9TOT-`{SP>wL=$# z)ZVxxr1ssvLu$X+j6B;zYU{3CuUiM$xN5!bp4j!eM)B))FK=G2yK3)x-I~ekbz2=; zuRHbPdff$=@%#1lx{eRl>*fT1)HUjKSFe4;T>V>3a`pRm%GJNnHCO) z-uUF(@y1s_j5qEjL>l{5h%~NLEz>7)2ikUO|N!xXgaK`L(_NN9GcGW>Ckim@X*1bsZA7? z#W*yr^p`_Z-KEICs}{}$ znc7qpv#mhGWh!R#*Q%JUiBmCK2Wae2F$>zOVpivjirL4e)@J(ctj$`qw>B$d3j|u5 zdCRTMR!y)r`+Ksr*;b(H3~MuCt+m;j_10!h_FJ2US-Y7Fc5dbk9Nf&!9NooNv3vasgn_ zeT`+uo@*?(IIpq%!)=YFdc+#bI^(cx${NcP(Q7QHzddL9{>wSbFM#UTbC%;Op0~76 zJ8!wL+Ih=2bCzi!TM{cuZm zYm4OU)&WPeTUSZTZe9OQcI&y1vRixRWViMOHowkpt^Osu^|evstb}Rftg1~PXVu_O zVDC7q?fb@Abv`uC$~t45)r`yIto{TlKO1K?`Q&G3~k!i1O7F#X}{FkrhTxjO?%B=Htl;k+q9oB$fkXaH}d+~wBH^&p~H)= z4jnhTICR|a=Fl-M(4ph05QmPjqcJ_*q2u)^hmMVBJ9O+b&!MBie20!kz|fTr9o^E@ zJKsI4XRFyh+}6W6+;$-Fymz>5u4}lh=WpS*u0i3pBZ9+iug}7==y2Pa3&L%WtP8h2 z7awlhHfMF$P0g2dKTui2-lD38{YqmE`_I-I_H#OG*w?Yuu;12A!+sAS>xub54SUm3 z8ulGWYuNit*RWr&H^9NdV1UCF;{gu0fyRvnIP^Cg;83H(0EfhG0~|6O2RPXE8Q|dV zKEPr2VB`%L;80$7u49+OM;+f(i*|aX6Yca{{b;8@fT4QPPB$AzJ1sDYc5<_hc6!?} z+9|b5w9{p~XeYHk(N1srM?3jUz2)S-#MpU#t!~a2o+dlnn*ZM0Uq8>~pKEhnZ{3{h zTI2a#*UfozT~pr9bv3OX<7%%J<7(X`#&vV^7}xDBV_Z9#$GCO_X4}QMPDwF#vpbdK z?sGrMUH%}+z2oa7_YJyx-LD((b${Gsulq9~%Ve*6pB8)F?JW1Y2X@};Zrf|G`@Yk4 z`Ww|s?XRMj+P|iLYXAObsr{4sr}mE;nA(31km8rx-zYS-f9p}H{ilYf_BWo8+FyNo zYX60OEC=oT>OLqLm?d}&(yr_=XtcJ+AT2$QL5U4L29;~#G03{T#~{NF9)ntS!tY%@ z2L0>cG3c+&D+jepSUIR#(#kY!pcFNR8|dYqp@mG zW$jgiYS&yf$S%>v^T8{3&kFC|J+(i$dk(1V;rYIqhv$qI9-e;!$2)s?j_T&&`M}=8 z)3>*W=M+~DPc;t@&vRibJpZ1!!gJ=d6`sqYR(Muiy~1uvhrj@@e@9|ID!}V&bbwdz zf&ed@g#li6K=kSWue6i1z3OjG^6IMB*L(NQ1B2(!F!0fxZQ#>pu7S_~1qMEimm2t7 zTWR1^b(?`tO94Sb@q41AV6LZ0UaK1px7`lxqp=3A$GGvDXV&3rRGn)#}D zHS?Vl(9HKK5E|XgcM>oa__P3dmNxTsif!iW5j5UcfAo0YGULblR*xL-dw4zOw~zNV z-#Om*!2a>RGY^jUoefO9I^MUzt?|AUACC8J^=!W1;GRjtraLDMThu#g*iEmbVPR8~ zhN;d-8de49x-@B6-pZt5gX5BhWyL2AE4L|WSabr?es2CX`n&lX4|emvq1*6Ml4#bEW&WoNuX zHMUNX2RS^K+j~Ek5Ac01|7X~9`LmGca_v#i<=tjHmxshWmmdRCfH^Ck%Y%}h%RldX zE^l|X{V1Ii`$nDD(HK2cUt{zvBaP8Z8)=Mw+(BcsYDbOH6FO^*?%}L4`a>W5-cMun z$N?Io%l@G;x*Sk_--fW?PHYJK18APQA#C;84PiUZZwTA=a6_2OvkhU_Wbt7;D#eG* z(u@x~+#o*ee8c##$(PE9w|HDWJmy9D@DE?ghkpjtzLpOUQ&kC{UsomkB@l0>5+2i5 zCEUAfxeht1*G8O+*rU2P{;g7 zQ$gsA%V<$R0HzxPyMS|;uh|0M%mczvx2H(g0p^(AqL20gzaB2$-Txv8e!zNQ$7g&K z0r(d%!Td<1A3q7gL0}?a3$TC2xL}#H71t&=qAqK3ISAOW2HOpA0Kx%1AU77zhXW@7 zf1o3v2^@ST2yX#~`UKFVzD#gJssbd}l?kDkFOT03ApH;} z6S~Zl2}6-?2W)Y3zu$E9FCcT8OxS_+BXAkZ>*M#?NKZ}?grVcm#<&DN?SUZlu7rCe zYsdsO@f@y!?C8iw!Ltqq86&KJCV);Vg5#ZEbCK%3>3HDLAGXaPJ#^K_% z6>t=|jCuJiK^Qa>UIRP^)&b42tO8b@yFw7QTVcJG@qQw}7ua7(5GDbyfof`~J8%Wd zP1T`cOyiS=!epR+Gnud}8=i;u?ePNT48c2!fQ!Iyq3pSFL0Asl27Ki3LEs4x517Ii zMgT##pk*KlSa4GidI2{9*=^iQa|dmUv?pMM=^ID`k&ZOL_VGc#KwBPx&R+@W$1<7F z44C^>5KaLqSQY`;0DFL2_-!pvtBg$O1N8e7<#>nk0H*JuQ(zY`0yz8uZ3wglqCUc} zfwI79;P3UgZxA;$r~&`rCIwI20k#sj4(MQh9^eKX!|&4o_5f)Ipw&re_#Ctkn4ZTb zzW^UWIuX#jh;OxFJ{+i(f&Pd2P{1G4&#$9z-+)&F5x@q_*8&CspRa-*NCa%J3BoMk z0w9|z2*coC(by)>R-=zt3qovd@ByuXGqtdf0?J^zvM%}vQms0IfDgn7w}FkoNZ^mf z*k+5+Cct6f6Xv%7Re)Yg1Ys#K{V()2AP3Wzk)F}O_k+++mIKq9;H=q*G zt}5>T0KQ=Q2;i+Q6SO9xe^0=E5AeqHlkw&L_I;2-? z%Y-?AA*SEek_lUYE|_l#?_7pF&OjEh%M<%UpffNQ_+t?EhrnXs=|FfKV1(&#q&9#m z&>#ivKL-0)=zIdOAGnA4mOvh+Hz2JWCKJvA^8pW_VmN#m*zO|;d4MtC53CrB?+^gh z0S~~|8{-NuLD&aGVfri(+y}aH!!lPv7zotB{3)PyU+4(S*Se$cA$^AVc|do}*9jEz zZ+3ZT1)u5*-vZR!QD+b61?fiMJpr893Du`-D#5MLGefXpON>2kg6mM!?PX z&={}^%liP2fz=DKFJFNBd64=5C+B1T36#Y$7o;_T8}kIg8uPn=FhExo``5{WP=cF@~fY$te7Z=_3rV|j=LB7jPmfBpuV2L=JPfD5nj z?DBm9g~NE@9>x$r_+4-T`+*La|Ae&M1MJ^`W5_oNm@*e-`xE*EDq;Eya0KWEw2Ofr zf#ztVS4ejQ{+RA?4(<2{v^W6wdLdnkvArBh4EE)^wS>rPC$1-&_f!8)B@NDoX5OcPxMcu?~o<~lYnkO`DJK(cu+LP zLs6daU!bfP{1CW>?KBvO1w00!-vA4N3xGZHRRLVhS*ZXm?;6 zu&^xr0ATOyjR4XBZ7g4`g7pC{E1<6<-O>ePIlvcq4V3LH6HINOAL#875QF&xm>&uB z#Pk)Q3ifN~mt&j?gaW^zt!4mwfGofk%W444R|qgBY!6^IP-!H_o!D^4Lov2PIs-@p zqOtr8&R6)|?O2d+$#39&9Xrs*IE@7rS^2MlWm%>p-o z>6mu}Y5+HpCk_}6Xj{WKJuy!HgN>2V|5{@k1Me&a0iT8y+5zD}aw}*Q_ymMlp^pNu zkq-|N2@Qs0zX4tuoBar zfDWd67@?m7|6qC=(!MpY&(o3#r-5piHmxobax~$mNaK(W1Zo3GnD2}{pOI>Yz*jQB zi9Ru51o{*3JQ(#v+8^mUAUz2CAEf_cpRpbo09aw!L|_%PI7A)(auEJ`2*-!O1;F+I zymUXb0erypD!?rneHidX-pas9>^Ib0;gwGXVe1ndmjPOUC$JF9QXgYXn1k(=jd2;$ zaLnI9+6?L7Uhw9j7zY8JfHy;+ePAGv=8J6sXkoex`qdskcpRV)Bp$^vCD7vt+7&Rw z^q#{QYXSo1=KJPfGpr%AM`O` z5YXRMCR}sD*aYbcpgxw}LTZL(4{=jzpWYZZ0=*n@{NyAPZou#YfG_sw$5=KBc`5)) z@Z0?>@G{_2Cd!TU;OBhQ7-2gBAUIRz~D%Hf;6gZ>}e&ALOnIK2H8p|wo zWWp1qUbSRGP2e=R4t6gaKplTUTHca0{rj4IZ3; zwg*-MOEKRMxQXejNOiX37#3IrxC2@{;9Yg$0r0kunm7hV+7sz!q*IX|1~RI_Ux4hY zXlJBVuNZo-do8i?!EO25I_CcXg)vw<%89f$Fay(NfHr9;6VMgYc_%Th1%iP?ST^7*`aAadQO3|pL)=*o ztO4*b7ok0tHE4vq03J9K+#AEAfJ#6zmfd>_e)NG$J!Qg7K(Cif=!o=n5A1V+lig)P zb)=O7=d;LbjM-Li8SqZ_}?t_ zi!;z7pm!R_4!}`BtI}l?J!*p z+W|1a{`n2kdzCQ`M_LPzW4Z&N0xYS7<1t_prfWxH-!dEf6u=2M4^)cAerOiCNh zX*A~3kj?;lVfqX(cq#T>({Wr2^aWCYl~ZvZ02l&vnTEDRs)lqskPUKg!w^09$G8e}tNUs7<=0MlLYT)#r&@EsIY(X9`peA5t0A1;0n*j%b z3V~YR-89byj zytxbd5l{ou9>6l-PaBN8fD1sdEi{eaHvr8r-CV$Nt0Tr=KrUeBg!2jxIJO5YF@FHu z*1$NREtY*m8isUWHIy9~Uln}qGzpp@Q;x7}vR)iM< zmw`r=P$yMr67T^kV%Z&}dw?mJ?gy9wwOU~O1?&Xa0}@}Fq2B@9fnKKQ$G}6ttU3A& za2lAHi1Q!lo3ifcYrgc(>39EPY=ZGi%0!H3khVn{3Czdzn+Z4v2o1Oc%Yki}ZwL%$ zsD*u&5&ZX0^yN7?Hvr57)PS_v7&8D}00BsjM*jv{1Fxa$_gXk605kx9ARSl+OsfH1 z1MD@s)wOY+7I=W^-N5^?XwL}rO{CAmWx_(B8>Tm5*?@5}p&W2zJjVY(6(AAw4|iZZ z4w&tP{{W|fTDwrD-S8x&I{^!{$x_Ta0FQv?=u5lTVf+oW1N<@nB@X)~Odmm737ESU z$3MUnOm_onXXCv1BP;{{%EEXSFvs)|EcbZ`uYQ1bLi!c3$8S53_IwS`J%)3uHh4&A zu+Rk$iv-|5PazO%f`0raA85o+2n$ku&1PKA!xNw;Yvx_ zF6SApY4&(Lz&CYtbSAOSo#uRI4|NJ?B zd}KQWp-eWR_|FeiLj+%tbeBq^=#;Vi$mGHqUSzC;=kg&V8CaC((q<%CA_WMEo}&GLm|k3S6qmc3fUUS3YE98&mm^k-5AE*-vg^_stu;kb1*^ zO5U-y#jp%7HU4qu?SQb(a5+Di%wSlIC)#1CFcukqva*61`@)4n@?D7S>ziPRSEnGa zBi7O-zhFa=^)oX3wBmvp6c*GD1IkillnbLulaX1+x{@=JExal@BT1gUE4G9(lANcN zoROq@v*e5Y{$fVIjy~I^hx1U#XM(WZHH8Aul*lSq-V_lgB zd9FxDq4zkTFR^We*JBD6$^ihG>V*%K52Rk%2d9UWY#i#P zXOs`5UfT&Lp_E)B_2R9{2U4$Yj#FPsu913qjF>>y>t84zNGfo{89XJ6N~*Ylz!n8N zc`1xt3+JJg4N|BhNPMt7|43NWnwd&c#I2c>@>yIksijv9Q4L0lnISHiRFf($n3SV} za||L{o7CedE|?S)B{rC;=#;o%Qc}g5qUua)8i`<2CX7J5Gzh2Elx`-{&uKBCq@J#|MXZ&y z!|nxAy1=9yEnN|zq?=LVLYZprhzTXl^u%=prRz$H`Hax~Gc1KGMxl~Hb-x`U6rW9@ zjLFJmmkS$8pPeb-RB5wQuV8%90*g|cDP!+$eWvXFFQ z=%}(`)Z?2PDY-W4`3n%%sOYZjijfLCpG@PG&&ZsAz)-lG70j&Y_01b99V8b9D;vam z{0ilQsK0+!E{J-&gRxR|qP{*)xghH4Pn8Rze$MV$P+~h#FCVO25cTo($_6nnf2mv$ z^>4MtO4W&aH$IbBeDD78a@+Zm*6?FyMely1e2`oy-=u`P{JbXCyYa2c;x}r+AnM(1 zlnbKX?W$Z5_3i-Wf~a>-S1#z==vuiT>fQU53!>hAN!cLQyZ==#hfH@k z5c6hVoV^^0v+DBvV@`!RD_pza#qtgavQjok(c8|~)vZJY zlbUL_{-!2&w}u=jc>bnvHGM`|hj;?g<$5UuOp(vU6Oa}yyNYfDQsHZFM1@=1*gYCaq^4J|9rZ6II`C1o`3!< z_@^_`HaMr|FVyIXo8*hjpwOdT=`x}srOC)NwzA}mq^4UXXC&R!=~ZH7NFl$KoRPG! zwd9P{>u;5uk@|NJ`x47YJ$YGaGO|8 z4L8Du; zDRCEBFhrrGMPftc!fJ7$`NuP2LPE?sDP^Ov+e~9Raq!~>yp`;i?gqrlg5e#ea(%iY&jIFEifbSA3d`PJ}5c|54LVw=Z zO+}DtNkIkmc?uzBEQFmTqn8tvre@^_;Z&gVVG1|6EhSto%<~kzcIFC>ULwQE8N9tk zhLbx44;C3t4iWAnGMro@%2#AKIYrTVPCx?m*C&xJJCpw(D#=Bu6!^t^54Hp?s z?y)ogmwk(uj2~23)a$|&(peG6fkrN*NT*>^%9ct)lF|*5SSga0qf{D_*lejZB(*fD zG$gq^sWc?LPQenJm?St;It`OzvQ!$9WUf>il4g$)cn@iD(qubYZ=Am$ng8OVf^W+z zeE&TKlzsV9DjeYUucDRVT}S5T8f)gJi^O8w2XehoEqviHA&n%vE6)Gufda=aR&OBnVS{yx7zF=2AyZ{@?7 zW!zOZjO@U4jFNTxHZE5-jAXn|*)Wo5%`hbkN%9+{Y#2%Fva(?$8?$gF3rSZWXDS;; z-Ti{HVbqm<$Ko#V;`X#o*lo*k>7D(hH~1F5%T582Jr5$qj=E(8S3B0oa%1*ZwzN=S}zg&!e#NzaMg zN=PpA1cdqL@#-6lfFVUr^F+Fi4?<3ex!uk2)9Hp=zg?pS6A1oI-O%}Ok<_cTH z1%GpZDWcX)YA-ugR4^%fiMZfzL*{9s)=Y{$EiRbUxpcayHIou;XNU}DL)ZJ_f=OZF zQKHsNY8o=LgkW~t<_PSu>3#e3?j)wDKqkyH{=>2#?iOs%)1k&tR_{*+W5 zQtu6rSQjTi_AD_wS*G}S%#YvAU^ili2^Mql)ir4p3MG5+qQdx^4;54O1|C$T?K?cE zNa5-+9GabU-i-$psXdYh6=^=62Nfy*0S_wDzy3T9%}yg_FCJ9n1q*mkkt5tM4OQ`9 ziBib9+WbLNT9S=`>aq`Ozzf}q%QawQQc+~5fxBDt`5F^3W{ zCup`rG7^6be#wx3v-~LRwxeg@RQ4UJ3>2*CkFuMM$}uq)?D%%dL}83ffN(kU~K^O_V}G z3azpp&jXfjzg+Mx&!Gq`_`r*fykpcBPL(D1$l^su{?Tt+HVxoi<05Y!VVq zi1J}fHXD@9=_GRDoKzB~m|STjq@TLS zB(({t$w3+kX)9bB2`O!ZG!oL?MQJ3YLX{Lr4NID|luE)B>LrbY^g3S}390uQNGuB% z;P^D`NueOU-jPBmtq%LFhrrGdSXN6f`zzHrXqVWp`@VzF`=ZO z8Dc_7KO4k^l6uaF2_@}3786R!DR)XlmmuBL78lA?(^gCGm9a8vcaT{ogxa6P=jY3&Zc+$y*+NZgdjw!GQFFI1?G+uP1)O22Sq~1zr zIJF%q+JP4xseC>!I#PZnFFJCAI%hex9bIx5$cv6#V+Aida*{It;FVYQa0~DJcf*DL zbOEyqPN@3izumiV2kD4#41|jcFFYs=K{zj4RM5{u;1|e_adT1G6$6LD*X4f>->xq@ zxZn_B9Wom@;Y9+9$C?J_H;3X=;C3)m)*Sbdtx3t*6t?JgUWx2-A-VL~nXT2iP)c^P ztwp8HPPSD2Vkz0lc9xblJK2hUMk(3JHkOn&JJ~|hOQmEd*>5O)cDCbha=Dc3B-;a} z%}%l$n~9Ut1-sbyFP?P8RgWRxu5=6)eth|b?8^TXioNC~P+%8I*w?s2pk?p;?mY@~ zy^#HgbiODp@z`@XpGHQOBRIY#0gPsWhO) zo0;{%J*CS`{qAe&GE;9Ge;dOd=X@_6{?mx5=u>(BAjnGDAVqJSct>=wTrj^YGMM$& zhvI^%_b$FCYR%M}d)yZlOuhS~xM1q-TONp7Gxh#~4@Cu&8meW93MO?N6C2FbGV+nA zHIsUbvqc4ynywKIuSX;CFw-u3Dy3UTs3loNa;l616O$vF$bvsLUPqe`+m}iNcB%a zWLBJL3)i~kOPu;tK1`wT@lwL&!XfeDOwDh_hLeV!UWu$bDR+(7aMJ5*vEigrn_Q8F zCv8TF4JSo@5*toB{Qb4a!qe4+jbg(|b05TplhVe#!QFbrNrs)fFL*=t|L$W?f~31t z5`_wD(?Lq>Wn^Oj3BNxj4Pkdc;8@gXB+w|d8=nxylYe8@=k8t=K3 zj67f%FEX}2zQ>1*{GrhYF4g>YTo1Bh56BC;MOPsp|I%C7w|98^^Zxi&(AFH?i zCxg2UrpgnOU|YK6`u<#&T0xic*b5w>*HGt=$9@Ghxm~lezo0?L@lruu?3q&sjKTtO zt*7f1PV9n%ZErV%;7=Aoa%&hBJ!nj@1nk_$8VP%-u2`3 z2o!7p_C4`19Nn;&U)@G%iDpuGsKNovt4O4f3*)3wFvX`xp&+GKttz1+r0^gq6r}76 zQYc8#?W#$r2q}5K6be#sbxjGSAmxThqhN|XCxwEP+M>FIijYDRK~d~23P0WuUfkCZ z>ubpb`{JZtS|WvdXYwVK3y*jbGG*J<;8tbQ`7VBhr23ZH+)79u5XX;@9KoO_w-S;+ zOyNgJZegm!t%T$qyLl2aCuvcOTM5Zm*6<@FmvO6I!svv~ZwBMoAOuGRbcKc;Dg5vC z-h-ej_GD`5sT59Qq|2F7E{xzy$=oKHCnfofQ5}vIC&$^vlaf5gv@XX|lIv{eNlCt= zSC3;U$$4UVQj+)7tW&ghCYdnxs;4-C$I?@GBTVEO=ZR3 z584Ml!JMLSvl(V0EB+~#;=>gVk;H=rkDExRVXn}+nZ!zwAIz3YLr(BODh+vnjj6;+ zk@{n$(va@oNTngghc=g3Dbo67=`>8`rY$6vhV(sADh(-Htz`)pH@`o$S%kJhQ0zIE zk_Z&)-pPju`sP8z6yC&4a@9!VkPW5vjeSh2*M{ z-WTv7Vv2tvorpAV*-CQNNcDR`WKrDt&F`Wu_!<}$%Tn7#!9az|4W$vt1sf>@Ow|L# z6Ofw6i6fJA%fK+=`JOQcoy?6prY0cK6I})k0jT8c=N;mNYq(-@T0#e~7 z5ET2!$d9k-@F`+!eF`MS9)p*LM4`^N{K({jo)xE(G1c1hAtMD(<3mPjPT@mF%C69c zOPi6(JMtkT#ryFgBlU0MLq<;UlouIuh1PAkYyfhI5q!wVEv|vA6mFro>9Fw8x^}x# zRj+swgB0TL{1*LFoe&E>P84RG?hw zEGCetd#Lh(q~t})2a;-EDj!G+HRz~xj~!TS+9O_^;AP zz8$A`l~fXPf^a)YB;*R3-6WBaLtKHmRK6HhrgxLkU`YwEwMCY z5hp-{=LQOsrd$4ygT2+U@IiTNmm-H3C50*!9W9kYF5Hzy!IW(1Dxo5z;t^6PNWr(I zP>_19`$(t=DK}OM1*!I{6be#okeh^xkXnyRqhJTnb^A&v1*vqb6be%4D^TE_#lRN&%veG zMHUQEC~2zLP`R*GTqsjfrkGIDPC;rkbX{A~p_b=65lnq?pABHDhne3B~apd#l&aZ}~2Kba)J5ZIusGDCee>aJleK zd^l53)gdAaPbz96Hk_2yOKdo)$xm!JDQddda8gyA*zj-45*tqHdLurZDXhv+k=vbA z)>LdbDQzIa`xY;mA5R0bTcp@`f&BAN3j2S1a}|4wRVe!+t?qla&nrHPp;zR{siW zvY^Yqf|~5fa(D>?0d`TT3k=B{S9N^^h4qXQ6DAk7D<8%zBS+aVvI9$hCF@2~4^uXb zWPDuNFp{W2fRcqI`S~jwM$)>cY#7PLI#9_%(pk|(%7#&Qzol##b>)yCoKq=oPwOn0 zVSlQ)wm~mcIENtX;)7V<9}WeulcwysM8EvuGJBCE8#spv)q?SKwloTbtXlG-k_(P} zsF?hQ@}MF~PUS&GGF`=kilm#$gNo$*ga;K#Tzv$GenqnH!h?#ez>g0V+hb1UK}D9a zjt3RlNCv1(i<2|^%^Rm~L-GgYqcHahAyy@Yj6euk2fH~c5W(#Iv~s~S1Xq^D1}ltX zvvd-<@IopHGYivDN!1~nSR{>v45Ex&Qc1`fe58?(DIAhULUvGVq@?PQ5v-O*LKg5r zDhU&R?@^MfL$co~jf4d6IJ$%(0K1jKAL>=2-23Wbu(jNwEj7wq{^ zF;!3GK}FiW%7cm&-ad>&TanJ!^PnQNYld?u6=^=42Nfw_bu5Qck^aZ>p<*uZga;LQ zL9Yl76(mQ6Nw!r_sXGtMvmvN;iRcWZ?!WceOOwP-AP?4OE@}MF)zu-Yd za;`s~L$i~dd-9+nIS2BfB00zMpdvY+=R?KhTrQGBvy+@#@t`6(PXtx_;zrot@17`J z%n2Z{l|rC!gA)_D5XpsI6FCquCm1+MG7Y|+gb_%6Z8o21SI4Q;t5E=&p=@5iqSP*!GUkl`3b!+9agGG3k_m$NG~o_ zp~PsZ6msE{Gzz9h|9KKBLVA2Ag@P3M=X?pJAWa%9kU&AI{96hI=~928gi??)&q$#l zZF((|Pzt)T_Eri7>2u;@38f%~IxoTF3MI*`a)Zvi=*Ss@dC`$eEaOMV_UDZca zat^zdT&hXFv5F5Fxki;$TuMfs5y*>-ImQD%WaJn2tGQH@+~O6;+Lm7EKOf%rk1fTp z<-6JZ9IkMP7d&X>!jv_VOT%12XRSmU@`KG%X~+pW#Yrp;c|e9#8dCqzbrMTMy8k4V zh7><{y~NUx)(tmEreP{il1lSU-|-SlL(0Ab8mrPPyWmMQ`QJ(nA_@j8R2?OqL@unB zO2QPqOBxC3`K&Y&Qu7mOB&6lC8zpTWQnHRT64G%qX(XiL&eBLo!+oWaFa?j2Mnd|H zkw!x5-2)P{ACLD#OLeyHGQzMsXD=I}OX{TyQz+JSlZ0@&;4eO$sdTT{aMGvVW|4&_ zWyXmOCrwt|B697dMnAFPq{Aa(!%2a)wu&q~X>XGFaHhI{#fFpKdMAi1JSi;;;W)uv zv}Ek?TL|9a-l1kU|pd!V7;z32~ z{cQ(_3X+l!@t`7A*WJmXRHX1fc~FtsE9~M>DmwQb%7cnr;1~}oas;p4B^Zh$ynwya zt8h0sg>an8FWOLw4_7$B9Ue4t;iYsM<^W|9C02^mUrj0vDc@Ks4XM7JR2oveqf{DF zyQfqdQu-LFG^Fwv=`?KLzE&y?se6}H8dCPTl4#g7tq406SM^F@d)KD^o!a;QRbE1+9t@uze z=WyadMXoWN2NgL+6b~wLi(NdZ$SH2~pdy#}%7cm=qTvA!{fgYdo(~msh9DkP~}X1C`g0lKuJAn=8&A(EFi&9v<|-)2m(b-%`WgSknhb7GsJ$*KilB8TRW}qi`0h zzl*=^zpNQGrnOE_c6RLDLNQ~c@J?w~35Tu1zoBXrIKKb8?@`r%fk)0f`_p?Rb|9z9eWf0l(AX2!=8@_~c zLFY8b5;8|=$B&RaC6FH>xymwrgybuy`4N({eBeh&-csibx2;U>;>eGX{ADsvLgp}A z`4N)GWbh*-m#K9YAAKy`913?6TLj=RqcB@4JV4`ZlNTDEY zosdF7Dtjq~g7l_)O~Mu-)tSy-^%WOPnp!F@nACI&!KNh%qu^O%gWE;!BEM^?V2DCVEyRY( zg-+r^nTj05gp!5^hzTVH`H2Z7{fre8O6rLc6H40oOH3#!XM>nf(#;-mp=^J2PE06i z=8l+9QcUGLGC{``-AeJT?jPT9Qx`wBaKnCPaQ=yz(tVoA20>QJ1}P-9TzoKY))gDf zWYy)asA7|__KOQ9X|=m2YRx3BSaHE5Fa7(X)=UB$DK3~K_E}soiEQEnQN^a6(^GN5 zB(wn!MXi~nRxJx7=c0vShZjrvakH=lE$B4rPH!GVf& z{?|(`RBY#ZCjCNRPM0 z6OalG-{YXMXfZkCCI@z-gU`2HOxbM@b_lUlE<~ZcRI#CQ!Q_LewKBEEhzTW~nST_q z)^7?E6H40h|0H6qq$=IdB0@<|>&1kUl16_Ku~yQM(N|HSOg(4CgpzKC3T2e95H=9du5g>k=vm?<@hKQZaJMp^Dn zK`Ia6PfQ+goImk5hbYIrDab#3_!E=6T<1?r-eX*zdsEPH_GJFVSg11-y+o8 zTBC$&6c?;e?=K!AEgw*>=8VRY_uBxQ!ka`oPk&t>-s!1vd zsW(g-390v%R1&6Mb4^LrA@xQ}BO&!Rs$QnJ6Iwq$)l>NXcN_?8i`)Nxf89jk1PT== z@F9{5*Le^zHJ8(pTs2a3d+9`^>Z#I+NZCiF6Op=w8j{GlQ>DpIhfE{A6S zc7)7>ic~$32Nmf%kp~qi{Rs~$(tOi89GacZk`3cQMLw{K2h}%6s9Oq0U}yK)Jt>6` zr!7Gt+aR+UJUCQOKGa8Vh~FrmK)qm3@8O<9eDvf&dY*bi{d__^L%fIjd3*Zng$9ib z@%Ay*8!DFvf13%_^9=FP3k;I$g^mmk4hoU`3^vuX3kuN-2*Pr|K;Iw}y&*xMVlkm@ zn>QaEl5M==DSK`A}&U<)l)X=#54phwAyEM0&nK{)2r&^iT{p$1ZxILs3Q(Rv*g3 zsv0;lc=!;#(SH8^dR{(ySPd(8_u}h?ruw-A@}Z2D5h_Z9Sp}gUDpcUN|E{F2`7O-q zg|-Mnd$Zc=HFM41fO_6-`+IqY^jEK>Ua7ErifsM!bA78=Xn$GT_TQ$RJ>^5K|DUZr zwRUIEP@loh)OTn-)_VPdg5`eL!UJTweb7cW&OP*o`-GuLA^GhR>hpi>y<3nR*O}+% z)IQiA$Atah-JMx^MmZSGl9GTLaioz(B6R@?NTNuQ5FwH?q%bqp-G%N#S68*_0;qem zHo5^2009yp36KDBr$7QAi5meD4G@KhFW=m3=T4mG!;bjo$i#;?*H3(4|9d7EeRgG| z+~{t^#qndsvK^Q&=lkmU>h-g^=!eTH>&m4{z9;S}Rtw$r(hwV_-dAbV

cqm-`~ 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�iN{1-@=B?$BnNqJc8+1_+M7Ppf7yF8s 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 z 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

<0>;K*z&&qpuvOo<(&vc`A_3n}(dUSux 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

|kFNaY>G+-~GmikQ)mk0B*S+U;%r& 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

s@_XUe({_G8 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 z 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%0D00Ctf4F<#fP;eRj12U5Yo9G{rIX1YQ{t205 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 Mdk~k3+2y2lSB;X*}H(p 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 - { - public static readonly AppClients Empty = new AppClients(); - - private AppClients() - { - } - - public AppClients(KeyValuePair[] items) - : base(items) - { - } - - [Pure] - public AppClients Revoke(string id) - { - Guard.NotNullOrEmpty(id, nameof(id)); - - return new AppClients(Without(id)); - } - - [Pure] - public AppClients Add(string id, AppClient client) - { - Guard.NotNullOrEmpty(id, nameof(id)); - Guard.NotNull(client, nameof(client)); - - if (ContainsKey(id)) - { - throw new ArgumentException("Id already exists.", nameof(id)); - } - - return new AppClients(With(id, client)); - } - - [Pure] - public AppClients Add(string id, string secret) - { - Guard.NotNullOrEmpty(id, nameof(id)); - - if (ContainsKey(id)) - { - throw new ArgumentException("Id already exists.", nameof(id)); - } - - return new AppClients(With(id, new AppClient(id, secret, Role.Editor))); - } - - [Pure] - public AppClients Rename(string id, string newName) - { - Guard.NotNullOrEmpty(id, nameof(id)); - - if (!TryGetValue(id, out var client)) - { - return this; - } - - return new AppClients(With(id, client.Rename(newName))); - } - - [Pure] - public AppClients Update(string id, string role) - { - Guard.NotNullOrEmpty(id, nameof(id)); - - if (!TryGetValue(id, out var client)) - { - return this; - } - - return new AppClients(With(id, client.Update(role))); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs deleted file mode 100644 index ee54ae6fb..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; - -namespace Squidex.Domain.Apps.Core.Apps -{ - public sealed class AppContributors : ArrayDictionary - { - public static readonly AppContributors Empty = new AppContributors(); - - private AppContributors() - { - } - - public AppContributors(KeyValuePair[] items) - : base(items) - { - } - - [Pure] - public AppContributors Assign(string contributorId, string role) - { - Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); - Guard.NotNullOrEmpty(role, nameof(role)); - - return new AppContributors(With(contributorId, role)); - } - - [Pure] - public AppContributors Remove(string contributorId) - { - Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); - - return new AppContributors(Without(contributorId)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs deleted file mode 100644 index b10c7e904..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Apps -{ - public sealed class AppImage - { - public string MimeType { get; } - - public string Etag { get; } - - public AppImage(string mimeType, string etag = null) - { - Guard.NotNullOrEmpty(mimeType, nameof(mimeType)); - - MimeType = mimeType; - - if (string.IsNullOrWhiteSpace(etag)) - { - Etag = RandomHash.Simple(); - } - else - { - Etag = etag; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs deleted file mode 100644 index 864961903..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Apps -{ - public sealed class AppPattern : Named - { - public string Pattern { get; } - - public string Message { get; } - - public AppPattern(string name, string pattern, string message = null) - : base(name) - { - Guard.NotNullOrEmpty(pattern, nameof(pattern)); - - Pattern = pattern; - - Message = message; - } - - [Pure] - public AppPattern Update(string newName, string newPattern, string newMessage) - { - return new AppPattern(newName, newPattern, newMessage); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs deleted file mode 100644 index cb9e13d3d..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; - -namespace Squidex.Domain.Apps.Core.Apps -{ - public sealed class AppPatterns : ArrayDictionary - { - public static readonly AppPatterns Empty = new AppPatterns(); - - private AppPatterns() - { - } - - public AppPatterns(KeyValuePair[] items) - : base(items) - { - } - - [Pure] - public AppPatterns Remove(Guid id) - { - return new AppPatterns(Without(id)); - } - - [Pure] - public AppPatterns Add(Guid id, string name, string pattern, string message) - { - var newPattern = new AppPattern(name, pattern, message); - - if (ContainsKey(id)) - { - throw new ArgumentException("Id already exists.", nameof(id)); - } - - return new AppPatterns(With(id, newPattern)); - } - - [Pure] - public AppPatterns Update(Guid id, string name, string pattern, string message) - { - Guard.NotNullOrEmpty(name, nameof(name)); - Guard.NotNullOrEmpty(pattern, nameof(pattern)); - - if (!TryGetValue(id, out var appPattern)) - { - return this; - } - - return new AppPatterns(With(id, appPattern.Update(name, pattern, message))); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs deleted file mode 100644 index ab23055bf..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Apps -{ - public sealed class AppPlan - { - public RefToken Owner { get; } - - public string PlanId { get; } - - public AppPlan(RefToken owner, string planId) - { - Guard.NotNull(owner, nameof(owner)); - Guard.NotNullOrEmpty(planId, nameof(planId)); - - Owner = owner; - - PlanId = planId; - } - - public static AppPlan Build(RefToken owner, string planId) - { - if (planId == null) - { - return null; - } - else - { - return new AppPlan(owner, planId); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs deleted file mode 100644 index 4649126c3..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Newtonsoft.Json; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Core.Apps.Json -{ - public class JsonAppPattern - { - [JsonProperty] - public string Name { get; set; } - - [JsonProperty] - public string Pattern { get; set; } - - [JsonProperty] - public string Message { get; set; } - - public JsonAppPattern() - { - } - - public JsonAppPattern(AppPattern pattern) - { - SimpleMapper.Map(pattern, this); - } - - public AppPattern ToPattern() - { - return new AppPattern(Name, Pattern, Message); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs deleted file mode 100644 index c455c07ff..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Newtonsoft.Json; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Apps.Json -{ - public sealed class JsonLanguagesConfig - { - [JsonProperty] - public Dictionary Languages { get; set; } - - [JsonProperty] - public Language Master { get; set; } - - public JsonLanguagesConfig() - { - } - - public JsonLanguagesConfig(LanguagesConfig value) - { - Languages = new Dictionary(value.Count); - - foreach (LanguageConfig config in value) - { - Languages.Add(config.Language, new JsonLanguageConfig(config)); - } - - Master = value.Master?.Language; - } - - public LanguagesConfig ToConfig() - { - var languagesConfig = new LanguageConfig[Languages?.Count ?? 0]; - - if (Languages != null) - { - var i = 0; - - foreach (var config in Languages) - { - languagesConfig[i++] = config.Value.ToConfig(config.Key); - } - } - - var result = LanguagesConfig.Build(languagesConfig); - - if (Master != null) - { - result = result.MakeMaster(Master); - } - - return result; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs deleted file mode 100644 index ac4bffde9..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Apps -{ - public sealed class LanguageConfig : IFieldPartitionItem - { - private readonly Language language; - private readonly Language[] languageFallbacks; - - public bool IsOptional { get; } - - public Language Language - { - get { return language; } - } - - public IEnumerable LanguageFallbacks - { - get { return languageFallbacks; } - } - - string IFieldPartitionItem.Key - { - get { return language.Iso2Code; } - } - - string IFieldPartitionItem.Name - { - get { return language.EnglishName; } - } - - IEnumerable IFieldPartitionItem.Fallback - { - get { return LanguageFallbacks.Select(x => x.Iso2Code); } - } - - public LanguageConfig(Language language, bool isOptional = false, IEnumerable fallback = null) - : this(language, isOptional, fallback?.ToArray()) - { - } - - public LanguageConfig(Language language, bool isOptional = false, params Language[] fallback) - { - Guard.NotNull(language, nameof(language)); - - IsOptional = isOptional; - - this.language = language; - this.languageFallbacks = fallback ?? Array.Empty(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs deleted file mode 100644 index cb83980f7..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs +++ /dev/null @@ -1,178 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Linq; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; - -namespace Squidex.Domain.Apps.Core.Apps -{ - public sealed class LanguagesConfig : IFieldPartitioning - { - public static readonly LanguagesConfig English = Build(Language.EN); - - private readonly ArrayDictionary languages; - private readonly LanguageConfig master; - - public LanguageConfig Master - { - get { return master; } - } - - IFieldPartitionItem IFieldPartitioning.Master - { - get { return master; } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return languages.Values.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return languages.Values.GetEnumerator(); - } - - public int Count - { - get { return languages.Count; } - } - - private LanguagesConfig(ArrayDictionary languages, LanguageConfig master, bool checkMaster = true) - { - if (checkMaster) - { - this.master = master ?? throw new InvalidOperationException("Config has no master language."); - } - - foreach (var languageConfig in languages.Values) - { - foreach (var fallback in languageConfig.LanguageFallbacks) - { - if (!languages.ContainsKey(fallback)) - { - var message = $"Config for language '{languageConfig.Language.Iso2Code}' contains unsupported fallback language '{fallback.Iso2Code}'"; - - throw new InvalidOperationException(message); - } - } - } - - this.languages = languages; - } - - public static LanguagesConfig Build(ICollection configs) - { - Guard.NotNull(configs, nameof(configs)); - - return new LanguagesConfig(configs.ToArrayDictionary(x => x.Language), configs.FirstOrDefault()); - } - - public static LanguagesConfig Build(params LanguageConfig[] configs) - { - return Build(configs?.ToList()); - } - - public static LanguagesConfig Build(params Language[] languages) - { - return Build(languages?.Select(x => new LanguageConfig(x)).ToList()); - } - - [Pure] - public LanguagesConfig MakeMaster(Language language) - { - Guard.NotNull(language, nameof(language)); - - return new LanguagesConfig(languages, languages[language]); - } - - [Pure] - public LanguagesConfig Set(Language language, bool isOptional = false, IEnumerable fallback = null) - { - Guard.NotNull(language, nameof(language)); - - return Set(new LanguageConfig(language, isOptional, fallback)); - } - - [Pure] - public LanguagesConfig Set(LanguageConfig config) - { - Guard.NotNull(config, nameof(config)); - - var newLanguages = - new ArrayDictionary(languages.With(config.Language, config)); - - var newMaster = Master?.Language == config.Language ? config : Master; - - return new LanguagesConfig(newLanguages, newMaster); - } - - [Pure] - public LanguagesConfig Remove(Language language) - { - Guard.NotNull(language, nameof(language)); - - var newLanguages = - languages.Values.Where(x => x.Language != language) - .Select(config => new LanguageConfig( - config.Language, - config.IsOptional, - config.LanguageFallbacks.Except(new[] { language }))) - .ToArrayDictionary(x => x.Language); - - var newMaster = - newLanguages.Values.FirstOrDefault(x => x.Language == Master.Language) ?? - newLanguages.Values.FirstOrDefault(); - - return new LanguagesConfig(newLanguages, newMaster); - } - - public bool Contains(Language language) - { - return language != null && languages.ContainsKey(language); - } - - public bool TryGetConfig(Language language, out LanguageConfig config) - { - return languages.TryGetValue(language, out config); - } - - public bool TryGetItem(string key, out IFieldPartitionItem item) - { - if (Language.IsValidLanguage(key) && languages.TryGetValue(key, out var value)) - { - item = value; - - return true; - } - else - { - item = null; - - return false; - } - } - - public PartitionResolver ToResolver() - { - return partitioning => - { - if (partitioning.Equals(Partitioning.Invariant)) - { - return InvariantPartitioning.Instance; - } - - return this; - }; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs deleted file mode 100644 index 1279367c1..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Linq; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using AllPermissions = Squidex.Shared.Permissions; - -namespace Squidex.Domain.Apps.Core.Apps -{ - public sealed class Role : Named - { - public const string Editor = "Editor"; - public const string Developer = "Developer"; - public const string Owner = "Owner"; - public const string Reader = "Reader"; - - public PermissionSet Permissions { get; } - - public bool IsDefault - { - get { return Roles.IsDefault(this); } - } - - public Role(string name, PermissionSet permissions) - : base(name) - { - Guard.NotNull(permissions, nameof(permissions)); - - Permissions = permissions; - } - - public Role(string name, params string[] permissions) - : this(name, new PermissionSet(permissions)) - { - } - - [Pure] - public Role Update(string[] permissions) - { - return new Role(Name, new PermissionSet(permissions)); - } - - public bool Equals(string name) - { - return name != null && name.Equals(Name, StringComparison.Ordinal); - } - - public Role ForApp(string app) - { - var result = new HashSet - { - AllPermissions.ForApp(AllPermissions.AppCommon, app) - }; - - if (Permissions.Any()) - { - var prefix = AllPermissions.ForApp(AllPermissions.App, app).Id; - - foreach (var permission in Permissions) - { - result.Add(new Permission(string.Concat(prefix, ".", permission.Id))); - } - } - - return new Role(Name, new PermissionSet(result)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs deleted file mode 100644 index bcdb9e226..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs +++ /dev/null @@ -1,179 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Linq; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; -using Squidex.Infrastructure.Security; -using Squidex.Shared; - -namespace Squidex.Domain.Apps.Core.Apps -{ - public sealed class Roles - { - private readonly ArrayDictionary inner; - - public static readonly IReadOnlyDictionary Defaults = new Dictionary - { - [Role.Owner] = - new Role(Role.Owner, new PermissionSet( - Clean(Permissions.App))), - [Role.Reader] = - new Role(Role.Reader, new PermissionSet( - Clean(Permissions.AppAssetsRead), - Clean(Permissions.AppContentsRead))), - [Role.Editor] = - new Role(Role.Editor, new PermissionSet( - Clean(Permissions.AppAssets), - Clean(Permissions.AppContents), - Clean(Permissions.AppRolesRead), - Clean(Permissions.AppWorkflowsRead))), - [Role.Developer] = - new Role(Role.Developer, new PermissionSet( - Clean(Permissions.AppApi), - Clean(Permissions.AppAssets), - Clean(Permissions.AppContents), - Clean(Permissions.AppPatterns), - Clean(Permissions.AppRolesRead), - Clean(Permissions.AppRules), - Clean(Permissions.AppSchemas), - Clean(Permissions.AppWorkflows))) - }; - - public static readonly Roles Empty = new Roles(new ArrayDictionary()); - - public int CustomCount - { - get { return inner.Count; } - } - - public Role this[string name] - { - get { return inner[name]; } - } - - public IEnumerable Custom - { - get { return inner.Values; } - } - - public IEnumerable All - { - get { return inner.Values.Union(Defaults.Values); } - } - - private Roles(ArrayDictionary roles) - { - inner = roles; - } - - public Roles(IEnumerable> items) - { - inner = new ArrayDictionary(Cleaned(items)); - } - - [Pure] - public Roles Remove(string name) - { - return new Roles(inner.Without(name)); - } - - [Pure] - public Roles Add(string name) - { - var newRole = new Role(name); - - if (inner.ContainsKey(name)) - { - throw new ArgumentException("Name already exists.", nameof(name)); - } - - if (IsDefault(name)) - { - return this; - } - - return new Roles(inner.With(name, newRole)); - } - - [Pure] - public Roles Update(string name, params string[] permissions) - { - Guard.NotNullOrEmpty(name, nameof(name)); - Guard.NotNull(permissions, nameof(permissions)); - - if (!inner.TryGetValue(name, out var role)) - { - return this; - } - - return new Roles(inner.With(name, role.Update(permissions))); - } - - public static bool IsDefault(string role) - { - return role != null && Defaults.ContainsKey(role); - } - - public static bool IsDefault(Role role) - { - return role != null && Defaults.ContainsKey(role.Name); - } - - public bool ContainsCustom(string name) - { - return inner.ContainsKey(name); - } - - public bool Contains(string name) - { - return inner.ContainsKey(name) || Defaults.ContainsKey(name); - } - - public bool TryGet(string app, string name, out Role value) - { - Guard.NotNull(app, nameof(app)); - - value = null; - - if (Defaults.TryGetValue(name, out var role) || inner.TryGetValue(name, out role)) - { - value = role.ForApp(app); - return true; - } - - return false; - } - - private static string Clean(string permission) - { - permission = Permissions.ForApp(permission).Id; - - var prefix = Permissions.ForApp(Permissions.App); - - if (permission.StartsWith(prefix.Id, StringComparison.OrdinalIgnoreCase)) - { - permission = permission.Substring(prefix.Id.Length); - } - - if (permission.Length == 0) - { - return Permission.Any; - } - - return permission.Substring(1); - } - - private static KeyValuePair[] Cleaned(IEnumerable> items) - { - return items.Where(x => !Defaults.ContainsKey(x.Key)).ToArray(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs b/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs deleted file mode 100644 index 61a90f23c..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using NodaTime; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Comments -{ - public sealed class Comment - { - public Guid Id { get; } - - public Instant Time { get; } - - public RefToken User { get; } - - public string Text { get; } - - public Comment(Guid id, Instant time, RefToken user, string text) - { - Guard.NotEmpty(id, nameof(id)); - Guard.NotNull(user, nameof(user)); - Guard.NotNull(text, nameof(text)); - - Id = id; - - Time = time; - Text = text; - - User = user; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs deleted file mode 100644 index fcfe4813e..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.Contents -{ - public abstract class ContentData : Dictionary, IEquatable> - { - public IEnumerable> ValidValues - { - get { return this.Where(x => x.Value != null); } - } - - protected ContentData(IEqualityComparer comparer) - : base(comparer) - { - } - - protected ContentData(int capacity, IEqualityComparer comparer) - : base(capacity, comparer) - { - } - - protected static TResult MergeTo(TResult target, params TResult[] sources) where TResult : ContentData - { - Guard.NotEmpty(sources, nameof(sources)); - - if (sources.Length == 1 || sources.Skip(1).All(x => ReferenceEquals(x, sources[0]))) - { - return sources[0]; - } - - foreach (var source in sources) - { - foreach (var otherValue in source) - { - var fieldValue = target.GetOrAddNew(otherValue.Key); - - foreach (var value in otherValue.Value) - { - fieldValue[value.Key] = value.Value; - } - } - } - - return target; - } - - protected static TResult Clean(TResult source, TResult target) where TResult : ContentData - { - foreach (var fieldValue in source.ValidValues) - { - var resultValue = new ContentFieldData(); - - foreach (var partitionValue in fieldValue.Value.Where(x => x.Value.Type != JsonValueType.Null)) - { - resultValue[partitionValue.Key] = partitionValue.Value; - } - - if (resultValue.Count > 0) - { - target[fieldValue.Key] = resultValue; - } - } - - return target; - } - - public override bool Equals(object obj) - { - return Equals(obj as ContentData); - } - - public bool Equals(ContentData other) - { - return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other)); - } - - public override int GetHashCode() - { - return this.DictionaryHashCode(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs deleted file mode 100644 index 28dca2ac1..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs +++ /dev/null @@ -1,71 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.Contents -{ - public sealed class ContentFieldData : Dictionary, IEquatable - { - public ContentFieldData() - : base(StringComparer.OrdinalIgnoreCase) - { - } - - public ContentFieldData AddValue(object value) - { - return AddJsonValue(JsonValue.Create(value)); - } - - public ContentFieldData AddValue(string key, object value) - { - return AddJsonValue(key, JsonValue.Create(value)); - } - - public ContentFieldData AddJsonValue(IJsonValue value) - { - this[InvariantPartitioning.Key] = value; - - return this; - } - - public ContentFieldData AddJsonValue(string key, IJsonValue value) - { - Guard.NotNullOrEmpty(key, nameof(key)); - - if (Language.IsValidLanguage(key)) - { - this[key] = value; - // this[string.Intern(key)] = value; - } - else - { - this[key] = value; - } - - return this; - } - - public override bool Equals(object obj) - { - return Equals(obj as ContentFieldData); - } - - public bool Equals(ContentFieldData other) - { - return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other)); - } - - public override int GetHashCode() - { - return this.DictionaryHashCode(); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs deleted file mode 100644 index 0ca33663e..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Contents -{ - public sealed class IdContentData : ContentData, IEquatable - { - public IdContentData() - : base(EqualityComparer.Default) - { - } - - public IdContentData(int capacity) - : base(capacity, EqualityComparer.Default) - { - } - - public static IdContentData Merge(params IdContentData[] contents) - { - return MergeTo(new IdContentData(), contents); - } - - public IdContentData MergeInto(IdContentData target) - { - return Merge(target, this); - } - - public IdContentData ToCleaned() - { - return Clean(this, new IdContentData()); - } - - public IdContentData AddField(long id, ContentFieldData data) - { - Guard.GreaterThan(id, 0, nameof(id)); - - this[id] = data; - - return this; - } - - public bool Equals(IdContentData other) - { - return base.Equals(other); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs deleted file mode 100644 index 3f170459e..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Newtonsoft.Json; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Newtonsoft; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.Contents.Json -{ - public sealed class ContentFieldDataConverter : JsonClassConverter - { - protected override void WriteValue(JsonWriter writer, ContentFieldData value, JsonSerializer serializer) - { - writer.WriteStartObject(); - - foreach (var kvp in value) - { - writer.WritePropertyName(kvp.Key); - - serializer.Serialize(writer, kvp.Value); - } - - writer.WriteEndObject(); - } - - protected override ContentFieldData ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) - { - var result = new ContentFieldData(); - - while (reader.Read()) - { - switch (reader.TokenType) - { - case JsonToken.PropertyName: - var propertyName = reader.Value.ToString(); - - if (!reader.Read()) - { - throw new JsonSerializationException("Unexpected end when reading Object."); - } - - var value = serializer.Deserialize(reader); - - if (Language.IsValidLanguage(propertyName) || propertyName == InvariantPartitioning.Key) - { - propertyName = string.Intern(propertyName); - } - - result[propertyName] = value; - break; - case JsonToken.EndObject: - return result; - } - } - - throw new JsonSerializationException("Unexpected end when reading Object."); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs deleted file mode 100644 index 48274e127..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschrnkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Collections.ObjectModel; -using Newtonsoft.Json; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Core.Contents.Json -{ - public class JsonWorkflowTransition - { - [JsonProperty] - public string Expression { get; set; } - - [JsonProperty] - public string Role { get; set; } - - [JsonProperty] - public List Roles { get; } - - public JsonWorkflowTransition() - { - } - - public JsonWorkflowTransition(WorkflowTransition client) - { - SimpleMapper.Map(client, this); - } - - public WorkflowTransition ToTransition() - { - var rolesList = Roles; - - if (!string.IsNullOrEmpty(Role)) - { - rolesList = new List { Role }; - } - - ReadOnlyCollection roles = null; - - if (rolesList != null && rolesList.Count > 0) - { - roles = new ReadOnlyCollection(rolesList); - } - - return new WorkflowTransition(Expression, roles); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs deleted file mode 100644 index d6afcd95f..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Contents -{ - public sealed class NamedContentData : ContentData, IEquatable - { - public NamedContentData() - : base(StringComparer.Ordinal) - { - } - - public NamedContentData(int capacity) - : base(capacity, StringComparer.Ordinal) - { - } - - public static NamedContentData Merge(params NamedContentData[] contents) - { - return MergeTo(new NamedContentData(), contents); - } - - public NamedContentData MergeInto(NamedContentData target) - { - return Merge(target, this); - } - - public NamedContentData ToCleaned() - { - return Clean(this, new NamedContentData()); - } - - public NamedContentData AddField(string name, ContentFieldData data) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - this[name] = data; - - return this; - } - - public bool Equals(NamedContentData other) - { - return base.Equals(other); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs deleted file mode 100644 index 32026fc44..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.ComponentModel; - -namespace Squidex.Domain.Apps.Core.Contents -{ - [TypeConverter(typeof(StatusConverter))] - public struct Status : IEquatable - { - public static readonly Status Archived = new Status("Archived"); - public static readonly Status Draft = new Status("Draft"); - public static readonly Status Published = new Status("Published"); - - private readonly string name; - - public string Name - { - get { return name ?? "Unknown"; } - } - - public Status(string name) - { - this.name = name; - } - - public override bool Equals(object obj) - { - return obj is Status status && Equals(status); - } - - public bool Equals(Status other) - { - return string.Equals(name, other.name); - } - - public override int GetHashCode() - { - return name?.GetHashCode() ?? 0; - } - - public override string ToString() - { - return Name; - } - - public static bool operator ==(Status lhs, Status rhs) - { - return lhs.Equals(rhs); - } - - public static bool operator !=(Status lhs, Status rhs) - { - return !lhs.Equals(rhs); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs deleted file mode 100644 index a7ba559c7..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.ComponentModel; -using System.Globalization; - -namespace Squidex.Domain.Apps.Core.Contents -{ - public sealed class StatusConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - return destinationType == typeof(string); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - return new Status(value?.ToString()); - } - - public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) - { - return value.ToString(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs deleted file mode 100644 index dae5dfd26..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs +++ /dev/null @@ -1,125 +0,0 @@ -// ========================================================================== -// 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.Contents -{ - public sealed class Workflow : Named - { - private const string DefaultName = "Unnamed"; - - public static readonly IReadOnlyDictionary EmptySteps = new Dictionary(); - public static readonly IReadOnlyList EmptySchemaIds = new List(); - public static readonly Workflow Default = CreateDefault(); - public static readonly Workflow Empty = new Workflow(default, EmptySteps); - - public IReadOnlyDictionary Steps { get; } = EmptySteps; - - public IReadOnlyList SchemaIds { get; } = EmptySchemaIds; - - public Status Initial { get; } - - public Workflow( - Status initial, - IReadOnlyDictionary steps, - IReadOnlyList schemaIds = null, - string name = null) - : base(name ?? DefaultName) - { - Initial = initial; - - if (steps != null) - { - Steps = steps; - } - - if (schemaIds != null) - { - SchemaIds = schemaIds; - } - } - - public static Workflow CreateDefault(string name = null) - { - return new Workflow( - Status.Draft, new Dictionary - { - [Status.Archived] = - new WorkflowStep( - new Dictionary - { - [Status.Draft] = new WorkflowTransition() - }, - StatusColors.Archived, true), - [Status.Draft] = - new WorkflowStep( - new Dictionary - { - [Status.Archived] = new WorkflowTransition(), - [Status.Published] = new WorkflowTransition() - }, - StatusColors.Draft), - [Status.Published] = - new WorkflowStep( - new Dictionary - { - [Status.Archived] = new WorkflowTransition(), - [Status.Draft] = new WorkflowTransition() - }, - StatusColors.Published) - }, null, name); - } - - public IEnumerable<(Status Status, WorkflowStep Step, WorkflowTransition Transition)> GetTransitions(Status status) - { - if (TryGetStep(status, out var step)) - { - foreach (var transition in step.Transitions) - { - yield return (transition.Key, Steps[transition.Key], transition.Value); - } - } - else if (TryGetStep(Initial, out var initial)) - { - yield return (Initial, initial, WorkflowTransition.Default); - } - } - - public bool TryGetTransition(Status from, Status to, out WorkflowTransition transition) - { - transition = null; - - if (TryGetStep(from, out var step)) - { - if (step.Transitions.TryGetValue(to, out transition)) - { - return true; - } - } - else if (to == Initial) - { - transition = WorkflowTransition.Default; - - return true; - } - - return false; - } - - public bool TryGetStep(Status status, out WorkflowStep step) - { - return Steps.TryGetValue(status, out step); - } - - public (Status Key, WorkflowStep) GetInitialStep() - { - return (Initial, Steps[Initial]); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs deleted file mode 100644 index 04eb595c5..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; - -namespace Squidex.Domain.Apps.Core.Contents -{ - public sealed class WorkflowStep - { - private static readonly IReadOnlyDictionary EmptyTransitions = new Dictionary(); - - public IReadOnlyDictionary Transitions { get; } - - public string Color { get; } - - public bool NoUpdate { get; } - - public WorkflowStep(IReadOnlyDictionary transitions = null, string color = null, bool noUpdate = false) - { - Transitions = transitions ?? EmptyTransitions; - - Color = color; - - NoUpdate = noUpdate; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs deleted file mode 100644 index 6466ece7a..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.ObjectModel; - -namespace Squidex.Domain.Apps.Core.Contents -{ - public sealed class WorkflowTransition - { - public static readonly WorkflowTransition Default = new WorkflowTransition(); - - public string Expression { get; } - - public ReadOnlyCollection Roles { get; } - - public WorkflowTransition(string expression = null, ReadOnlyCollection roles = null) - { - Expression = expression; - - Roles = roles; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs deleted file mode 100644 index 353323008..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs +++ /dev/null @@ -1,83 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Linq; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; - -namespace Squidex.Domain.Apps.Core.Contents -{ - public sealed class Workflows : ArrayDictionary - { - public static readonly Workflows Empty = new Workflows(); - - private Workflows() - { - } - - public Workflows(KeyValuePair[] items) - : base(items) - { - } - - [Pure] - public Workflows Remove(Guid id) - { - return new Workflows(Without(id)); - } - - [Pure] - public Workflows Add(Guid workflowId, string name) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - return new Workflows(With(workflowId, Workflow.CreateDefault(name))); - } - - [Pure] - public Workflows Set(Workflow workflow) - { - Guard.NotNull(workflow, nameof(workflow)); - - return new Workflows(With(Guid.Empty, workflow)); - } - - [Pure] - public Workflows Set(Guid id, Workflow workflow) - { - Guard.NotNull(workflow, nameof(workflow)); - - return new Workflows(With(id, workflow)); - } - - [Pure] - public Workflows Update(Guid id, Workflow workflow) - { - Guard.NotNull(workflow, nameof(workflow)); - - if (id == Guid.Empty) - { - return Set(workflow); - } - - if (!ContainsKey(id)) - { - return this; - } - - return new Workflows(With(id, workflow)); - } - - public Workflow GetFirst() - { - return Values.FirstOrDefault() ?? Workflow.Default; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs b/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs deleted file mode 100644 index a50e9beff..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace Squidex.Domain.Apps.Core -{ - public sealed class InvariantPartitioning : IFieldPartitioning, IFieldPartitionItem - { - public static readonly InvariantPartitioning Instance = new InvariantPartitioning(); - public static readonly string Key = "iv"; - - public int Count - { - get { return 1; } - } - - public IFieldPartitionItem Master - { - get { return this; } - } - - string IFieldPartitionItem.Key - { - get { return Key; } - } - - string IFieldPartitionItem.Name - { - get { return "Invariant"; } - } - - bool IFieldPartitionItem.IsOptional - { - get { return false; } - } - - IEnumerable IFieldPartitionItem.Fallback - { - get { return Enumerable.Empty(); } - } - - private InvariantPartitioning() - { - } - - public bool TryGetItem(string key, out IFieldPartitionItem item) - { - var isFound = string.Equals(key, Key, StringComparison.OrdinalIgnoreCase); - - item = isFound ? this : null; - - return isFound; - } - - IEnumerator IEnumerable.GetEnumerator() - { - yield return this; - } - - IEnumerator IEnumerable.GetEnumerator() - { - yield return this; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Named.cs b/src/Squidex.Domain.Apps.Core.Model/Named.cs deleted file mode 100644 index fd76c4e8f..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Named.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core -{ - public abstract class Named - { - public string Name { get; } - - protected Named(string name) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - Name = name; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs b/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs deleted file mode 100644 index 8190674f1..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core -{ - public delegate IFieldPartitioning PartitionResolver(Partitioning key); - - public sealed class Partitioning : IEquatable - { - public static readonly Partitioning Invariant = new Partitioning("invariant"); - public static readonly Partitioning Language = new Partitioning("language"); - - public string Key { get; } - - public Partitioning(string key) - { - Guard.NotNullOrEmpty(key, nameof(key)); - - Key = key; - } - - public override bool Equals(object obj) - { - return Equals(obj as Partitioning); - } - - public bool Equals(Partitioning other) - { - return string.Equals(other?.Key, Key, StringComparison.OrdinalIgnoreCase); - } - - public override int GetHashCode() - { - return Key.GetHashCode(); - } - - public override string ToString() - { - return Key; - } - - public static Partitioning FromString(string value) - { - var isLanguage = string.Equals(value, Language.Key, StringComparison.OrdinalIgnoreCase); - - return isLanguage ? Language : Invariant; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs b/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs deleted file mode 100644 index 089fc0ae6..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; - -namespace Squidex.Domain.Apps.Core -{ - public static class PartitioningExtensions - { - private static readonly HashSet AllowedPartitions = new HashSet(StringComparer.OrdinalIgnoreCase) - { - Partitioning.Language.Key, - Partitioning.Invariant.Key - }; - - public static bool IsValidPartitioning(this string value) - { - return value == null || AllowedPartitions.Contains(value); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs deleted file mode 100644 index 7517186a4..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs +++ /dev/null @@ -1,116 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Rules -{ - public sealed class Rule : Cloneable - { - private RuleTrigger trigger; - private RuleAction action; - private string name; - private bool isEnabled = true; - - public string Name - { - get { return name; } - } - - public RuleTrigger Trigger - { - get { return trigger; } - } - - public RuleAction Action - { - get { return action; } - } - - public bool IsEnabled - { - get { return isEnabled; } - } - - public Rule(RuleTrigger trigger, RuleAction action) - { - Guard.NotNull(trigger, nameof(trigger)); - Guard.NotNull(action, nameof(action)); - - this.trigger = trigger; - this.trigger.Freeze(); - - this.action = action; - this.action.Freeze(); - } - - [Pure] - public Rule Rename(string name) - { - return Clone(clone => - { - clone.name = name; - }); - } - - [Pure] - public Rule Enable() - { - return Clone(clone => - { - clone.isEnabled = true; - }); - } - - [Pure] - public Rule Disable() - { - return Clone(clone => - { - clone.isEnabled = false; - }); - } - - [Pure] - public Rule Update(RuleTrigger newTrigger) - { - Guard.NotNull(newTrigger, nameof(newTrigger)); - - if (newTrigger.GetType() != trigger.GetType()) - { - throw new ArgumentException("New trigger has another type.", nameof(newTrigger)); - } - - newTrigger.Freeze(); - - return Clone(clone => - { - clone.trigger = newTrigger; - }); - } - - [Pure] - public Rule Update(RuleAction newAction) - { - Guard.NotNull(newAction, nameof(newAction)); - - if (newAction.GetType() != action.GetType()) - { - throw new ArgumentException("New action has another type.", nameof(newAction)); - } - - newAction.Freeze(); - - return Clone(clone => - { - clone.action = newAction; - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs deleted file mode 100644 index 76086d174..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Core.Rules.Triggers -{ - public sealed class ContentChangedTriggerSchemaV2 : Freezable - { - public Guid SchemaId { get; set; } - - public string Condition { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs deleted file mode 100644 index 77cf55f72..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs +++ /dev/null @@ -1,91 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class ArrayField : RootField, IArrayField - { - private FieldCollection fields = FieldCollection.Empty; - - public IReadOnlyList Fields - { - get { return fields.Ordered; } - } - - public IReadOnlyDictionary FieldsById - { - get { return fields.ById; } - } - - public IReadOnlyDictionary FieldsByName - { - get { return fields.ByName; } - } - - public FieldCollection FieldCollection - { - get { return fields; } - } - - public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties properties = null, IFieldSettings settings = null) - : base(id, name, partitioning, properties, settings) - { - } - - public ArrayField(long id, string name, Partitioning partitioning, NestedField[] fields, ArrayFieldProperties properties = null, IFieldSettings settings = null) - : this(id, name, partitioning, properties, settings) - { - Guard.NotNull(fields, nameof(fields)); - - this.fields = new FieldCollection(fields); - } - - [Pure] - public ArrayField DeleteField(long fieldId) - { - return Updatefields(f => f.Remove(fieldId)); - } - - [Pure] - public ArrayField ReorderFields(List ids) - { - return Updatefields(f => f.Reorder(ids)); - } - - [Pure] - public ArrayField AddField(NestedField field) - { - return Updatefields(f => f.Add(field)); - } - - [Pure] - public ArrayField UpdateField(long fieldId, Func updater) - { - return Updatefields(f => f.Update(fieldId, updater)); - } - - private ArrayField Updatefields(Func, FieldCollection> updater) - { - var newFields = updater(fields); - - if (ReferenceEquals(newFields, fields)) - { - return this; - } - - return Clone(clone => - { - clone.fields = newFields; - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs deleted file mode 100644 index 27ee0a32f..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class ArrayFieldProperties : FieldProperties - { - public int? MinItems { get; set; } - - public int? MaxItems { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IArrayField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.Array(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - throw new NotSupportedException(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs deleted file mode 100644 index 5bdb42606..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.ObjectModel; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class AssetsFieldProperties : FieldProperties - { - public bool MustBeImage { get; set; } - - public int? MinItems { get; set; } - - public int? MaxItems { get; set; } - - public int? MinWidth { get; set; } - - public int? MaxWidth { get; set; } - - public int? MinHeight { get; set; } - - public int? MaxHeight { get; set; } - - public int? MinSize { get; set; } - - public int? MaxSize { get; set; } - - public int? AspectWidth { get; set; } - - public int? AspectHeight { get; set; } - - public bool AllowDuplicates { get; set; } - - public bool ResolveImage { get; set; } - - public ReadOnlyCollection AllowedExtensions { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.Assets(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.Assets(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs deleted file mode 100644 index abf76f41d..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class BooleanFieldProperties : FieldProperties - { - public bool? DefaultValue { get; set; } - - public bool InlineEditable { get; set; } - - public BooleanFieldEditor Editor { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.Boolean(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.Boolean(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs deleted file mode 100644 index ca80bf2f4..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class DateTimeFieldProperties : FieldProperties - { - public Instant? MaxValue { get; set; } - - public Instant? MinValue { get; set; } - - public Instant? DefaultValue { get; set; } - - public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; } - - public DateTimeFieldEditor Editor { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.DateTime(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.DateTime(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs deleted file mode 100644 index 4450ef2d1..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs +++ /dev/null @@ -1,169 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Linq; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class FieldCollection : Cloneable> where T : IField - { - public static readonly FieldCollection Empty = new FieldCollection(); - - private static readonly Dictionary EmptyById = new Dictionary(); - private static readonly Dictionary EmptyByString = new Dictionary(); - - private T[] fieldsOrdered; - private Dictionary fieldsById; - private Dictionary fieldsByName; - - public IReadOnlyList Ordered - { - get { return fieldsOrdered; } - } - - public IReadOnlyDictionary ById - { - get - { - if (fieldsById == null) - { - if (fieldsOrdered.Length == 0) - { - fieldsById = EmptyById; - } - else - { - fieldsById = fieldsOrdered.ToDictionary(x => x.Id); - } - } - - return fieldsById; - } - } - - public IReadOnlyDictionary ByName - { - get - { - if (fieldsByName == null) - { - if (fieldsOrdered.Length == 0) - { - fieldsByName = EmptyByString; - } - else - { - fieldsByName = fieldsOrdered.ToDictionary(x => x.Name); - } - } - - return fieldsByName; - } - } - - private FieldCollection() - { - fieldsOrdered = Array.Empty(); - } - - public FieldCollection(T[] fields) - { - Guard.NotNull(fields, nameof(fields)); - - fieldsOrdered = fields; - } - - protected override void OnCloned() - { - fieldsById = null; - fieldsByName = null; - } - - [Pure] - public FieldCollection Remove(long fieldId) - { - if (!ById.TryGetValue(fieldId, out _)) - { - return this; - } - - return Clone(clone => - { - clone.fieldsOrdered = fieldsOrdered.Where(x => x.Id != fieldId).ToArray(); - }); - } - - [Pure] - public FieldCollection Reorder(List ids) - { - Guard.NotNull(ids, nameof(ids)); - - if (ids.Count != fieldsOrdered.Length || ids.Any(x => !ById.ContainsKey(x))) - { - throw new ArgumentException("Ids must cover all fields.", nameof(ids)); - } - - return Clone(clone => - { - clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToArray(); - }); - } - - [Pure] - public FieldCollection Add(T field) - { - Guard.NotNull(field, nameof(field)); - - if (ByName.ContainsKey(field.Name)) - { - throw new ArgumentException($"A field with name '{field.Name}' already exists.", nameof(field)); - } - - if (ById.ContainsKey(field.Id)) - { - throw new ArgumentException($"A field with id {field.Id} already exists.", nameof(field)); - } - - return Clone(clone => - { - clone.fieldsOrdered = clone.fieldsOrdered.Union(Enumerable.Repeat(field, 1)).ToArray(); - }); - } - - [Pure] - public FieldCollection Update(long fieldId, Func updater) - { - Guard.NotNull(updater, nameof(updater)); - - if (!ById.TryGetValue(fieldId, out var field)) - { - return this; - } - - var newField = updater(field); - - if (ReferenceEquals(newField, field)) - { - return this; - } - - if (!(newField is T)) - { - throw new InvalidOperationException($"Field must be of type {typeof(T)}"); - } - - return Clone(clone => - { - clone.fieldsOrdered = clone.fieldsOrdered.Select(x => ReferenceEquals(x, field) ? newField : x).ToArray(); - }); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs deleted file mode 100644 index 36ae3e210..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.ObjectModel; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public abstract class FieldProperties : NamedElementPropertiesBase - { - public bool IsRequired { get; set; } - - public bool IsListField { get; set; } - - public bool IsReferenceField { get; set; } - - public string Placeholder { get; set; } - - public string EditorUrl { get; set; } - - public ReadOnlyCollection Tags { get; set; } - - public abstract T Accept(IFieldPropertiesVisitor visitor); - - public abstract T Accept(IFieldVisitor visitor, IField field); - - public abstract RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null); - - public abstract NestedField CreateNestedField(long id, string name, IFieldSettings settings = null); - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs deleted file mode 100644 index 1938ad663..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs +++ /dev/null @@ -1,236 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public static class Fields - { - public static RootField Array(long id, string name, Partitioning partitioning, params NestedField[] fields) - { - return new ArrayField(id, name, partitioning, fields); - } - - public static ArrayField Array(long id, string name, Partitioning partitioning, ArrayFieldProperties properties = null, IFieldSettings settings = null) - { - return new ArrayField(id, name, partitioning, properties, settings); - } - - public static RootField Assets(long id, string name, Partitioning partitioning, AssetsFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField Boolean(long id, string name, Partitioning partitioning, BooleanFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField DateTime(long id, string name, Partitioning partitioning, DateTimeFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField Geolocation(long id, string name, Partitioning partitioning, GeolocationFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField Json(long id, string name, Partitioning partitioning, JsonFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField Number(long id, string name, Partitioning partitioning, NumberFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField References(long id, string name, Partitioning partitioning, ReferencesFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField String(long id, string name, Partitioning partitioning, StringFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField Tags(long id, string name, Partitioning partitioning, TagsFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField UI(long id, string name, Partitioning partitioning, UIFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static NestedField Assets(long id, string name, AssetsFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField Boolean(long id, string name, BooleanFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField DateTime(long id, string name, DateTimeFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField Geolocation(long id, string name, GeolocationFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField Json(long id, string name, JsonFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField Number(long id, string name, NumberFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField References(long id, string name, ReferencesFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField String(long id, string name, StringFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField Tags(long id, string name, TagsFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField UI(long id, string name, UIFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, Func handler = null, ArrayFieldProperties properties = null, IFieldSettings settings = null) - { - var field = Array(id, name, partitioning, properties, settings); - - if (handler != null) - { - field = handler(field); - } - - return schema.AddField(field); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - - 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)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs deleted file mode 100644 index b6f0a9804..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class GeolocationFieldProperties : FieldProperties - { - public GeolocationFieldEditor Editor { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.Geolocation(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.Geolocation(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs deleted file mode 100644 index 3a7a90900..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Newtonsoft.Json; -using Squidex.Infrastructure; -using P = Squidex.Domain.Apps.Core.Partitioning; - -namespace Squidex.Domain.Apps.Core.Schemas.Json -{ - public sealed class JsonFieldModel : IFieldSettings - { - [JsonProperty] - public long Id { get; set; } - - [JsonProperty] - public string Name { get; set; } - - [JsonProperty] - public string Partitioning { get; set; } - - [JsonProperty] - public bool IsHidden { get; set; } - - [JsonProperty] - public bool IsLocked { get; set; } - - [JsonProperty] - public bool IsDisabled { get; set; } - - [JsonProperty] - public FieldProperties Properties { get; set; } - - [JsonProperty] - public JsonNestedFieldModel[] Children { get; set; } - - public RootField ToField() - { - var partitioning = P.FromString(Partitioning); - - if (Properties is ArrayFieldProperties arrayProperties) - { - var nested = Children?.Map(n => n.ToNestedField()) ?? Array.Empty(); - - return new ArrayField(Id, Name, partitioning, nested, arrayProperties, this); - } - else - { - return Properties.CreateRootField(Id, Name, partitioning, this); - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs deleted file mode 100644 index 54c31c88f..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs +++ /dev/null @@ -1,111 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Core.Schemas.Json -{ - public sealed class JsonSchemaModel - { - [JsonProperty] - public string Name { get; set; } - - [JsonProperty] - public string Category { get; set; } - - [JsonProperty] - public bool IsSingleton { get; set; } - - [JsonProperty] - public bool IsPublished { get; set; } - - [JsonProperty] - public SchemaProperties Properties { get; set; } - - [JsonProperty] - public SchemaScripts Scripts { get; set; } - - [JsonProperty] - public JsonFieldModel[] Fields { get; set; } - - [JsonProperty] - public Dictionary PreviewUrls { get; set; } - - public JsonSchemaModel() - { - } - - public JsonSchemaModel(Schema schema) - { - SimpleMapper.Map(schema, this); - - Fields = - schema.Fields.Select(x => - new JsonFieldModel - { - Id = x.Id, - Name = x.Name, - Children = CreateChildren(x), - IsHidden = x.IsHidden, - IsLocked = x.IsLocked, - IsDisabled = x.IsDisabled, - Partitioning = x.Partitioning.Key, - Properties = x.RawProperties - }).ToArray(); - - PreviewUrls = schema.PreviewUrls.ToDictionary(x => x.Key, x => x.Value); - } - - private static JsonNestedFieldModel[] CreateChildren(IField field) - { - if (field is ArrayField arrayField) - { - return arrayField.Fields.Select(x => - new JsonNestedFieldModel - { - Id = x.Id, - Name = x.Name, - IsHidden = x.IsHidden, - IsLocked = x.IsLocked, - IsDisabled = x.IsDisabled, - Properties = x.RawProperties - }).ToArray(); - } - - return null; - } - - public Schema ToSchema() - { - var fields = Fields.Map(f => f.ToField()) ?? Array.Empty(); - - var schema = new Schema(Name, fields, Properties, IsPublished, IsSingleton); - - if (!string.IsNullOrWhiteSpace(Category)) - { - schema = schema.ChangeCategory(Category); - } - - if (Scripts != null) - { - schema = schema.ConfigureScripts(Scripts); - } - - if (PreviewUrls?.Count > 0) - { - schema = schema.ConfigurePreviewUrls(PreviewUrls); - } - - return schema; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs deleted file mode 100644 index 5dc24c564..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class JsonFieldProperties : FieldProperties - { - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.Json(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.Json(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs deleted file mode 100644 index 9b3b92aba..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public abstract class NamedElementPropertiesBase : Freezable - { - public string Label { get; set; } - - public string Hints { get; set; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs deleted file mode 100644 index 9643c2ea4..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public abstract class NestedField : Cloneable, INestedField - { - private readonly long fieldId; - private readonly string fieldName; - private bool isDisabled; - private bool isHidden; - private bool isLocked; - - public long Id - { - get { return fieldId; } - } - - public string Name - { - get { return fieldName; } - } - - public bool IsLocked - { - get { return isLocked; } - } - - public bool IsHidden - { - get { return isHidden; } - } - - public bool IsDisabled - { - get { return isDisabled; } - } - - public abstract FieldProperties RawProperties { get; } - - protected NestedField(long id, string name, IFieldSettings settings = null) - { - Guard.NotNullOrEmpty(name, nameof(name)); - Guard.GreaterThan(id, 0, nameof(id)); - - fieldId = id; - fieldName = name; - - if (settings != null) - { - isLocked = settings.IsLocked; - isHidden = settings.IsHidden; - isDisabled = settings.IsDisabled; - } - } - - [Pure] - public NestedField Lock() - { - return Clone(clone => - { - clone.isLocked = true; - }); - } - - [Pure] - public NestedField Hide() - { - return Clone(clone => - { - clone.isHidden = true; - }); - } - - [Pure] - public NestedField Show() - { - return Clone(clone => - { - clone.isHidden = false; - }); - } - - [Pure] - public NestedField Disable() - { - return Clone(clone => - { - clone.isDisabled = true; - }); - } - - [Pure] - public NestedField Enable() - { - return Clone(clone => - { - clone.isDisabled = false; - }); - } - - public abstract T Accept(IFieldVisitor visitor); - - public abstract NestedField Update(FieldProperties newProperties); - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs deleted file mode 100644 index 808de167b..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public class NestedField : NestedField, IField where T : FieldProperties, new() - { - private T properties; - - public T Properties - { - get { return properties; } - } - - public override FieldProperties RawProperties - { - get { return properties; } - } - - public NestedField(long id, string name, T properties = null, IFieldSettings settings = null) - : base(id, name, settings) - { - SetProperties(properties ?? new T()); - } - - [Pure] - public override NestedField Update(FieldProperties newProperties) - { - var typedProperties = ValidateProperties(newProperties); - - return Clone>(clone => - { - clone.SetProperties(typedProperties); - }); - } - - private void SetProperties(T newProperties) - { - properties = newProperties; - properties.Freeze(); - } - - private T ValidateProperties(FieldProperties newProperties) - { - Guard.NotNull(newProperties, nameof(newProperties)); - - if (!(newProperties is T typedProperties)) - { - throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties)); - } - - return typedProperties; - } - - public override TResult Accept(IFieldVisitor visitor) - { - return properties.Accept(visitor, this); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs deleted file mode 100644 index dedbe213f..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.ObjectModel; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class NumberFieldProperties : FieldProperties - { - public ReadOnlyCollection AllowedValues { get; set; } - - public double? MaxValue { get; set; } - - public double? MinValue { get; set; } - - public double? DefaultValue { get; set; } - - public bool IsUnique { get; set; } - - public bool InlineEditable { get; set; } - - public NumberFieldEditor Editor { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.Number(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.Number(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs deleted file mode 100644 index bca51a0cc..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class ReferencesFieldProperties : FieldProperties - { - public int? MinItems { get; set; } - - public int? MaxItems { get; set; } - - public bool ResolveReference { get; set; } - - public bool AllowDuplicates { get; set; } - - public ReferencesFieldEditor Editor { get; set; } - - public ReadOnlyCollection SchemaIds { get; set; } - - public Guid SchemaId - { - set - { - if (value != default) - { - SchemaIds = new ReadOnlyCollection(new List { value }); - } - else - { - SchemaIds = null; - } - } - } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.References(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.References(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs deleted file mode 100644 index 6c21a1054..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs +++ /dev/null @@ -1,122 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public abstract class RootField : Cloneable, IRootField - { - private readonly long fieldId; - private readonly string fieldName; - private readonly Partitioning partitioning; - private bool isDisabled; - private bool isHidden; - private bool isLocked; - - public long Id - { - get { return fieldId; } - } - - public string Name - { - get { return fieldName; } - } - - public bool IsLocked - { - get { return isLocked; } - } - - public bool IsHidden - { - get { return isHidden; } - } - - public bool IsDisabled - { - get { return isDisabled; } - } - - public Partitioning Partitioning - { - get { return partitioning; } - } - - public abstract FieldProperties RawProperties { get; } - - protected RootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - Guard.NotNullOrEmpty(name, nameof(name)); - Guard.GreaterThan(id, 0, nameof(id)); - Guard.NotNull(partitioning, nameof(partitioning)); - - fieldId = id; - fieldName = name; - - this.partitioning = partitioning; - - if (settings != null) - { - isLocked = settings.IsLocked; - isHidden = settings.IsHidden; - isDisabled = settings.IsDisabled; - } - } - - [Pure] - public RootField Lock() - { - return Clone(clone => - { - clone.isLocked = true; - }); - } - - [Pure] - public RootField Hide() - { - return Clone(clone => - { - clone.isHidden = true; - }); - } - - [Pure] - public RootField Show() - { - return Clone(clone => - { - clone.isHidden = false; - }); - } - - [Pure] - public RootField Disable() - { - return Clone(clone => - { - clone.isDisabled = true; - }); - } - - [Pure] - public RootField Enable() - { - return Clone(clone => - { - clone.isDisabled = false; - }); - } - - public abstract T Accept(IFieldVisitor visitor); - - public abstract RootField Update(FieldProperties newProperties); - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs deleted file mode 100644 index cbe6716d0..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public class RootField : RootField, IField where T : FieldProperties, new() - { - private T properties; - - public T Properties - { - get { return properties; } - } - - public override FieldProperties RawProperties - { - get { return properties; } - } - - public RootField(long id, string name, Partitioning partitioning, T properties = null, IFieldSettings settings = null) - : base(id, name, partitioning, settings) - { - SetProperties(properties ?? new T()); - } - - [Pure] - public override RootField Update(FieldProperties newProperties) - { - var typedProperties = ValidateProperties(newProperties); - - return Clone>(clone => - { - clone.SetProperties(typedProperties); - }); - } - - private void SetProperties(T newProperties) - { - properties = newProperties; - properties.Freeze(); - } - - private T ValidateProperties(FieldProperties newProperties) - { - Guard.NotNull(newProperties, nameof(newProperties)); - - if (!(newProperties is T typedProperties)) - { - throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties)); - } - - return typedProperties; - } - - public override TResult Accept(IFieldVisitor visitor) - { - return properties.Accept(visitor, this); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs deleted file mode 100644 index 7b188283a..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs +++ /dev/null @@ -1,201 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class Schema : Cloneable - { - private static readonly Dictionary EmptyPreviewUrls = new Dictionary(); - private readonly string name; - private readonly bool isSingleton; - private string category; - private FieldCollection fields = FieldCollection.Empty; - private IReadOnlyDictionary previewUrls = EmptyPreviewUrls; - private SchemaScripts scripts = new SchemaScripts(); - private SchemaProperties properties; - private bool isPublished; - - public string Name - { - get { return name; } - } - - public string Category - { - get { return category; } - } - - public bool IsPublished - { - get { return isPublished; } - } - - public bool IsSingleton - { - get { return isSingleton; } - } - - public IReadOnlyList Fields - { - get { return fields.Ordered; } - } - - public IReadOnlyDictionary FieldsById - { - get { return fields.ById; } - } - - public IReadOnlyDictionary FieldsByName - { - get { return fields.ByName; } - } - - public IReadOnlyDictionary PreviewUrls - { - get { return previewUrls; } - } - - public FieldCollection FieldCollection - { - get { return fields; } - } - - public SchemaScripts Scripts - { - get { return scripts; } - } - - public SchemaProperties Properties - { - get { return properties; } - } - - public Schema(string name, SchemaProperties properties = null, bool isSingleton = false) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - this.name = name; - - this.properties = properties ?? new SchemaProperties(); - this.properties.Freeze(); - - this.isSingleton = isSingleton; - } - - public Schema(string name, RootField[] fields, SchemaProperties properties, bool isPublished, bool isSingleton = false) - : this(name, properties, isSingleton) - { - Guard.NotNull(fields, nameof(fields)); - - this.fields = new FieldCollection(fields); - - this.isPublished = isPublished; - } - - [Pure] - public Schema Update(SchemaProperties newProperties) - { - Guard.NotNull(newProperties, nameof(newProperties)); - - return Clone(clone => - { - clone.properties = newProperties; - clone.properties.Freeze(); - }); - } - - [Pure] - public Schema ConfigureScripts(SchemaScripts newScripts) - { - return Clone(clone => - { - clone.scripts = newScripts ?? new SchemaScripts(); - clone.scripts.Freeze(); - }); - } - - [Pure] - public Schema Publish() - { - return Clone(clone => - { - clone.isPublished = true; - }); - } - - [Pure] - public Schema Unpublish() - { - return Clone(clone => - { - clone.isPublished = false; - }); - } - - [Pure] - public Schema ChangeCategory(string newCategory) - { - return Clone(clone => - { - clone.category = newCategory; - }); - } - - [Pure] - public Schema ConfigurePreviewUrls(IReadOnlyDictionary newPreviewUrls) - { - return Clone(clone => - { - clone.previewUrls = newPreviewUrls ?? EmptyPreviewUrls; - }); - } - - [Pure] - public Schema DeleteField(long fieldId) - { - return UpdateFields(f => f.Remove(fieldId)); - } - - [Pure] - public Schema ReorderFields(List ids) - { - return UpdateFields(f => f.Reorder(ids)); - } - - [Pure] - public Schema AddField(RootField field) - { - return UpdateFields(f => f.Add(field)); - } - - [Pure] - public Schema UpdateField(long fieldId, Func updater) - { - return UpdateFields(f => f.Update(fieldId, updater)); - } - - private Schema UpdateFields(Func, FieldCollection> updater) - { - var newFields = updater(fields); - - if (ReferenceEquals(newFields, fields)) - { - return this; - } - - return Clone(clone => - { - clone.fields = newFields; - }); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs deleted file mode 100644 index 1b28964ac..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.ObjectModel; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class StringFieldProperties : FieldProperties - { - public ReadOnlyCollection AllowedValues { get; set; } - - public int? MinLength { get; set; } - - public int? MaxLength { get; set; } - - public bool IsUnique { get; set; } - - public bool InlineEditable { get; set; } - - public string DefaultValue { get; set; } - - public string Pattern { get; set; } - - public string PatternMessage { get; set; } - - public StringFieldEditor Editor { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.String(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.String(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs deleted file mode 100644 index d81043aba..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.ObjectModel; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class TagsFieldProperties : FieldProperties - { - public ReadOnlyCollection AllowedValues { get; set; } - - public int? MinItems { get; set; } - - public int? MaxItems { get; set; } - - public TagsFieldEditor Editor { get; set; } - - public TagsFieldNormalization Normalization { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.Tags(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.Tags(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs deleted file mode 100644 index 3fd109ce8..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class UIFieldProperties : FieldProperties - { - public UIFieldEditor Editor { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return new NestedField(id, name, this, settings); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj deleted file mode 100644 index 3dc19b23e..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - netstandard2.0 - Squidex.Domain.Apps.Core - 7.3 - - - full - True - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs deleted file mode 100644 index 8a9ba47cc..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs +++ /dev/null @@ -1,150 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ConvertContent -{ - public static class ContentConverter - { - private static readonly Func KeyNameResolver = f => f.Name; - private static readonly Func KeyIdResolver = f => f.Id; - - public static string ToFullText(this ContentData data, int maxTotalLength = 1024 * 1024, int maxFieldLength = 1000, string separator = " ") - { - var stringBuilder = new StringBuilder(); - - foreach (var value in data.Values.SelectMany(x => x.Values)) - { - AppendText(value, stringBuilder, maxFieldLength, separator, false); - } - - var result = stringBuilder.ToString(); - - if (result.Length > maxTotalLength) - { - result = result.Substring(0, maxTotalLength); - } - - return result; - } - - private static void AppendText(IJsonValue value, StringBuilder stringBuilder, int maxFieldLength, string separator, bool allowObjects) - { - if (value.Type == JsonValueType.String) - { - var text = value.ToString(); - - if (text.Length <= maxFieldLength) - { - if (stringBuilder.Length > 0) - { - stringBuilder.Append(separator); - } - - stringBuilder.Append(text); - } - } - else if (value is JsonArray array) - { - foreach (var item in array) - { - AppendText(item, stringBuilder, maxFieldLength, separator, true); - } - } - else if (value is JsonObject obj && allowObjects) - { - foreach (var item in obj.Values) - { - AppendText(item, stringBuilder, maxFieldLength, separator, true); - } - } - } - - public static NamedContentData ConvertId2Name(this IdContentData content, Schema schema, params FieldConverter[] converters) - { - Guard.NotNull(schema, nameof(schema)); - - var result = new NamedContentData(content.Count); - - return ConvertInternal(content, result, schema.FieldsById, KeyNameResolver, converters); - } - - public static IdContentData ConvertId2Id(this IdContentData content, Schema schema, params FieldConverter[] converters) - { - Guard.NotNull(schema, nameof(schema)); - - var result = new IdContentData(content.Count); - - return ConvertInternal(content, result, schema.FieldsById, KeyIdResolver, converters); - } - - public static NamedContentData ConvertName2Name(this NamedContentData content, Schema schema, params FieldConverter[] converters) - { - Guard.NotNull(schema, nameof(schema)); - - var result = new NamedContentData(content.Count); - - return ConvertInternal(content, result, schema.FieldsByName, KeyNameResolver, converters); - } - - public static IdContentData ConvertName2Id(this NamedContentData content, Schema schema, params FieldConverter[] converters) - { - Guard.NotNull(schema, nameof(schema)); - - var result = new IdContentData(content.Count); - - return ConvertInternal(content, result, schema.FieldsByName, KeyIdResolver, converters); - } - - private static TDict2 ConvertInternal( - TDict1 source, - TDict2 target, - IReadOnlyDictionary fields, - Func targetKey, params FieldConverter[] converters) - where TDict1 : IDictionary - where TDict2 : IDictionary - { - foreach (var fieldKvp in source) - { - if (!fields.TryGetValue(fieldKvp.Key, out var field)) - { - continue; - } - - var newvalue = fieldKvp.Value; - - if (converters != null) - { - foreach (var converter in converters) - { - newvalue = converter(newvalue, field); - - if (newvalue == null) - { - break; - } - } - } - - if (newvalue != null) - { - target.Add(targetKey(field), newvalue); - } - } - - return target; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs deleted file mode 100644 index 8a6e82895..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ConvertContent -{ - public static class ContentConverterFlat - { - public static object ToFlatLanguageModel(this NamedContentData content, LanguagesConfig languagesConfig, IReadOnlyCollection languagePreferences = null) - { - Guard.NotNull(languagesConfig, nameof(languagesConfig)); - - if (languagePreferences == null || languagePreferences.Count == 0) - { - return content; - } - - if (languagePreferences.Count == 1 && languagesConfig.TryGetConfig(languagePreferences.First(), out var languageConfig)) - { - languagePreferences = languagePreferences.Union(languageConfig.LanguageFallbacks).ToList(); - } - - var result = new Dictionary(); - - foreach (var fieldValue in content) - { - var fieldData = fieldValue.Value; - - foreach (var language in languagePreferences) - { - if (fieldData.TryGetValue(language, out var value) && value.Type != JsonValueType.Null) - { - result[fieldValue.Key] = value; - - break; - } - } - } - - return result; - } - - public static Dictionary ToFlatten(this NamedContentData content) - { - var result = new Dictionary(); - - foreach (var fieldValue in content) - { - var fieldData = fieldValue.Value; - - if (fieldData.Count == 1) - { - result[fieldValue.Key] = fieldData.Values.First(); - } - else - { - result[fieldValue.Key] = fieldData; - } - } - - return result; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs deleted file mode 100644 index f9e35bb58..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs +++ /dev/null @@ -1,373 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -#pragma warning disable RECS0002 // Convert anonymous method to method group - -namespace Squidex.Domain.Apps.Core.ConvertContent -{ - public delegate ContentFieldData FieldConverter(ContentFieldData data, IRootField field); - - public static class FieldConverters - { - private static readonly Func KeyNameResolver = f => f.Name; - private static readonly Func KeyIdResolver = f => f.Id.ToString(); - - private static readonly Func FieldByIdResolver = - (f, k) => long.TryParse(k, out var id) ? f.FieldsById.GetOrDefault(id) : null; - - private static readonly Func FieldByNameResolver = - (f, k) => f.FieldsByName.GetOrDefault(k); - - public static FieldConverter ExcludeHidden() - { - return (data, field) => !field.IsForApi() ? null : data; - } - - public static FieldConverter ExcludeChangedTypes() - { - return (data, field) => - { - foreach (var value in data.Values) - { - if (value.Type == JsonValueType.Null) - { - continue; - } - - try - { - JsonValueConverter.ConvertValue(field, value); - } - catch - { - return null; - } - } - - return data; - }; - } - - public static FieldConverter ResolveAssetUrls(IReadOnlyCollection fields, IAssetUrlGenerator urlGenerator) - { - if (fields?.Any() != true) - { - return (data, field) => data; - } - - var isAll = fields.First() == "*"; - - return (data, field) => - { - if (field is IField && (isAll || fields.Contains(field.Name))) - { - foreach (var partition in data) - { - if (partition.Value is JsonArray array) - { - for (var i = 0; i < array.Count; i++) - { - var id = array[i].ToString(); - - array[i] = JsonValue.Create(urlGenerator.GenerateUrl(id)); - } - } - } - } - - return data; - }; - } - - public static FieldConverter ResolveInvariant(LanguagesConfig config) - { - var codeForInvariant = InvariantPartitioning.Key; - var codeForMasterLanguage = config.Master.Language.Iso2Code; - - return (data, field) => - { - if (field.Partitioning.Equals(Partitioning.Invariant)) - { - var result = new ContentFieldData(); - - if (data.TryGetValue(codeForInvariant, out var value)) - { - result[codeForInvariant] = value; - } - else if (data.TryGetValue(codeForMasterLanguage, out value)) - { - result[codeForInvariant] = value; - } - else if (data.Count > 0) - { - result[codeForInvariant] = data.Values.First(); - } - - return result; - } - - return data; - }; - } - - public static FieldConverter ResolveLanguages(LanguagesConfig config) - { - var codeForInvariant = InvariantPartitioning.Key; - - return (data, field) => - { - if (field.Partitioning.Equals(Partitioning.Language)) - { - var result = new ContentFieldData(); - - foreach (var languageConfig in config) - { - var languageCode = languageConfig.Key; - - if (data.TryGetValue(languageCode, out var value)) - { - result[languageCode] = value; - } - else if (languageConfig == config.Master && data.TryGetValue(codeForInvariant, out value)) - { - result[languageCode] = value; - } - } - - return result; - } - - return data; - }; - } - - public static FieldConverter ResolveFallbackLanguages(LanguagesConfig config) - { - var master = config.Master; - - return (data, field) => - { - if (field.Partitioning.Equals(Partitioning.Language)) - { - foreach (var languageConfig in config) - { - var languageCode = languageConfig.Key; - - if (!data.TryGetValue(languageCode, out var value)) - { - var dataFound = false; - - foreach (var fallback in languageConfig.Fallback) - { - if (data.TryGetValue(fallback, out value)) - { - data[languageCode] = value; - dataFound = true; - break; - } - } - - if (!dataFound && languageConfig != master) - { - if (data.TryGetValue(master.Language, out value)) - { - data[languageCode] = value; - } - } - } - } - } - - return data; - }; - } - - public static FieldConverter FilterLanguages(LanguagesConfig config, IEnumerable languages) - { - if (languages?.Any() != true) - { - return (data, field) => data; - } - - var languageSet = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var language in languages) - { - if (config.Contains(language.Iso2Code)) - { - languageSet.Add(language.Iso2Code); - } - } - - if (languageSet.Count == 0) - { - languageSet.Add(config.Master.Language.Iso2Code); - } - - return (data, field) => - { - if (field.Partitioning.Equals(Partitioning.Language)) - { - var result = new ContentFieldData(); - - foreach (var languageCode in languageSet) - { - if (data.TryGetValue(languageCode, out var value)) - { - result[languageCode] = value; - } - } - - return result; - } - - return data; - }; - } - - public static FieldConverter ForNestedName2Name(params ValueConverter[] converters) - { - return ForNested(FieldByNameResolver, KeyNameResolver, converters); - } - - public static FieldConverter ForNestedName2Id(params ValueConverter[] converters) - { - return ForNested(FieldByNameResolver, KeyIdResolver, converters); - } - - public static FieldConverter ForNestedId2Name(params ValueConverter[] converters) - { - return ForNested(FieldByIdResolver, KeyNameResolver, converters); - } - - public static FieldConverter ForNestedId2Id(params ValueConverter[] converters) - { - return ForNested(FieldByIdResolver, KeyIdResolver, converters); - } - - private static FieldConverter ForNested( - Func fieldResolver, - Func keyResolver, - params ValueConverter[] converters) - { - return (data, field) => - { - if (field is IArrayField arrayField) - { - var result = new ContentFieldData(); - - foreach (var partition in data) - { - if (!(partition.Value is JsonArray array)) - { - continue; - } - - var newArray = JsonValue.Array(); - - foreach (var item in array.OfType()) - { - var newItem = JsonValue.Object(); - - foreach (var kvp in item) - { - var nestedField = fieldResolver(arrayField, kvp.Key); - - if (nestedField == null) - { - continue; - } - - var newValue = kvp.Value; - - var isUnset = false; - - if (converters != null) - { - foreach (var converter in converters) - { - newValue = converter(newValue, nestedField); - - if (ReferenceEquals(newValue, Value.Unset)) - { - isUnset = true; - break; - } - } - } - - if (!isUnset) - { - newItem.Add(keyResolver(nestedField), newValue); - } - } - - newArray.Add(newItem); - } - - result.Add(partition.Key, newArray); - } - - return result; - } - - return data; - }; - } - - public static FieldConverter ForValues(params ValueConverter[] converters) - { - return (data, field) => - { - if (!(field is IArrayField)) - { - var result = new ContentFieldData(); - - foreach (var partition in data) - { - var newValue = partition.Value; - - var isUnset = false; - - if (converters != null) - { - foreach (var converter in converters) - { - newValue = converter(newValue, field); - - if (ReferenceEquals(newValue, Value.Unset)) - { - isUnset = true; - break; - } - } - } - - if (!isUnset) - { - result.Add(partition.Key, newValue); - } - } - - return result; - } - - return data; - }; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs deleted file mode 100644 index 196225e64..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.EnrichContent -{ - public sealed class ContentEnricher - { - private readonly Schema schema; - private readonly PartitionResolver partitionResolver; - - public ContentEnricher(Schema schema, PartitionResolver partitionResolver) - { - Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(partitionResolver, nameof(partitionResolver)); - - this.schema = schema; - - this.partitionResolver = partitionResolver; - } - - public void Enrich(NamedContentData data) - { - Guard.NotNull(data, nameof(data)); - - foreach (var field in schema.Fields) - { - var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData()); - var fieldPartition = partitionResolver(field.Partitioning); - - foreach (var partitionItem in fieldPartition) - { - Enrich(field, fieldData, partitionItem); - } - - if (fieldData.Count > 0) - { - data[field.Name] = fieldData; - } - } - } - - private static void Enrich(IField field, ContentFieldData fieldData, IFieldPartitionItem partitionItem) - { - Guard.NotNull(fieldData, nameof(fieldData)); - - var defaultValue = DefaultValueFactory.CreateDefaultValue(field, SystemClock.Instance.GetCurrentInstant()); - - if (field.RawProperties.IsRequired || defaultValue == null || defaultValue.Type == JsonValueType.Null) - { - return; - } - - var key = partitionItem.Key; - - if (!fieldData.TryGetValue(key, out var value) || ShouldApplyDefaultValue(field, value)) - { - fieldData.AddJsonValue(key, defaultValue); - } - } - - private static bool ShouldApplyDefaultValue(IField field, IJsonValue value) - { - return value.Type == JsonValueType.Null || (field is IField && value is JsonScalar s && string.IsNullOrEmpty(s.Value)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs deleted file mode 100644 index 5c3379739..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs +++ /dev/null @@ -1,97 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Globalization; -using NodaTime; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.EnrichContent -{ - public sealed class DefaultValueFactory : IFieldVisitor - { - private readonly Instant now; - - private DefaultValueFactory(Instant now) - { - this.now = now; - } - - public static IJsonValue CreateDefaultValue(IField field, Instant now) - { - Guard.NotNull(field, nameof(field)); - - return field.Accept(new DefaultValueFactory(now)); - } - - public IJsonValue Visit(IArrayField field) - { - return JsonValue.Array(); - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Array(); - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Create(field.Properties.DefaultValue); - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Null; - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Null; - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Create(field.Properties.DefaultValue); - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Array(); - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Create(field.Properties.DefaultValue); - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Array(); - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Null; - } - - public IJsonValue Visit(IField field) - { - if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now) - { - return JsonValue.Create(now.ToString()); - } - - if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today) - { - return JsonValue.Create($"{now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}T00:00:00Z"); - } - - return JsonValue.Create(field.Properties.DefaultValue?.ToString()); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs b/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs deleted file mode 100644 index 68e916936..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs +++ /dev/null @@ -1,223 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; - -namespace Squidex.Domain.Apps.Core.EventSynchronization -{ - public static class SchemaSynchronizer - { - public static IEnumerable Synchronize(this Schema source, Schema target, IJsonSerializer serializer, Func idGenerator, SchemaSynchronizationOptions options = null) - { - Guard.NotNull(source, nameof(source)); - Guard.NotNull(serializer, nameof(serializer)); - Guard.NotNull(idGenerator, nameof(idGenerator)); - - if (target == null) - { - yield return new SchemaDeleted(); - } - else - { - options = options ?? new SchemaSynchronizationOptions(); - - SchemaEvent E(SchemaEvent @event) - { - return @event; - } - - if (!source.Properties.EqualsJson(target.Properties, serializer)) - { - yield return E(new SchemaUpdated { Properties = target.Properties }); - } - - if (!source.Category.StringEquals(target.Category)) - { - yield return E(new SchemaCategoryChanged { Name = target.Category }); - } - - if (!source.Scripts.EqualsJson(target.Scripts, serializer)) - { - yield return E(new SchemaScriptsConfigured { Scripts = target.Scripts }); - } - - if (!source.PreviewUrls.EqualsDictionary(target.PreviewUrls)) - { - yield return E(new SchemaPreviewUrlsConfigured { PreviewUrls = target.PreviewUrls.ToDictionary(x => x.Key, x => x.Value) }); - } - - if (source.IsPublished != target.IsPublished) - { - yield return target.IsPublished ? - E(new SchemaPublished()) : - E(new SchemaUnpublished()); - } - - var events = SyncFields(source.FieldCollection, target.FieldCollection, serializer, idGenerator, CanUpdateRoot, null, options); - - foreach (var @event in events) - { - yield return E(@event); - } - } - } - - private static IEnumerable SyncFields( - FieldCollection source, - FieldCollection target, - IJsonSerializer serializer, - Func idGenerator, - Func canUpdate, - NamedId parentId, SchemaSynchronizationOptions options) where T : class, IField - { - FieldEvent E(FieldEvent @event) - { - @event.ParentFieldId = parentId; - - return @event; - } - - var sourceIds = new List>(source.Ordered.Select(x => x.NamedId())); - var sourceNames = sourceIds.Select(x => x.Name).ToList(); - - if (!options.NoFieldDeletion) - { - foreach (var sourceField in source.Ordered) - { - if (!target.ByName.TryGetValue(sourceField.Name, out _)) - { - var id = sourceField.NamedId(); - - sourceIds.Remove(id); - sourceNames.Remove(id.Name); - - yield return E(new FieldDeleted { FieldId = id }); - } - } - } - - foreach (var targetField in target.Ordered) - { - NamedId id = null; - - var canCreateField = true; - - if (source.ByName.TryGetValue(targetField.Name, out var sourceField)) - { - canCreateField = false; - - id = sourceField.NamedId(); - - if (canUpdate(sourceField, targetField)) - { - if (!sourceField.RawProperties.EqualsJson(targetField.RawProperties, serializer)) - { - yield return E(new FieldUpdated { FieldId = id, Properties = targetField.RawProperties }); - } - } - else if (!sourceField.IsLocked && !options.NoFieldRecreation) - { - canCreateField = true; - - sourceIds.Remove(id); - sourceNames.Remove(id.Name); - - yield return E(new FieldDeleted { FieldId = id }); - } - } - - if (canCreateField) - { - var partitioning = (string)null; - - if (targetField is IRootField rootField) - { - partitioning = rootField.Partitioning.Key; - } - - id = NamedId.Of(idGenerator(), targetField.Name); - - yield return new FieldAdded - { - Name = targetField.Name, - ParentFieldId = parentId, - Partitioning = partitioning, - Properties = targetField.RawProperties, - FieldId = id - }; - - sourceIds.Add(id); - sourceNames.Add(id.Name); - } - - if (id != null && (sourceField == null || CanUpdate(sourceField, targetField))) - { - if (!targetField.IsLocked.BoolEquals(sourceField?.IsLocked)) - { - yield return E(new FieldLocked { FieldId = id }); - } - - if (!targetField.IsHidden.BoolEquals(sourceField?.IsHidden)) - { - yield return targetField.IsHidden ? - E(new FieldHidden { FieldId = id }) : - E(new FieldShown { FieldId = id }); - } - - if (!targetField.IsDisabled.BoolEquals(sourceField?.IsDisabled)) - { - yield return targetField.IsDisabled ? - E(new FieldDisabled { FieldId = id }) : - E(new FieldEnabled { FieldId = id }); - } - - if ((sourceField == null || sourceField is IArrayField) && targetField is IArrayField targetArrayField) - { - var fields = ((IArrayField)sourceField)?.FieldCollection ?? FieldCollection.Empty; - - var events = SyncFields(fields, targetArrayField.FieldCollection, serializer, idGenerator, CanUpdate, id, options); - - foreach (var @event in events) - { - yield return @event; - } - } - } - } - - if (sourceNames.Count > 1) - { - var targetNames = target.Ordered.Select(x => x.Name); - - if (sourceNames.Intersect(targetNames).Count() == target.Ordered.Count && !sourceNames.SequenceEqual(targetNames)) - { - var fieldIds = targetNames.Select(x => sourceIds.FirstOrDefault(y => y.Name == x).Id).ToList(); - - yield return new SchemaFieldsReordered { FieldIds = fieldIds, ParentFieldId = parentId }; - } - } - } - - private static bool CanUpdateRoot(IRootField source, IRootField target) - { - return CanUpdate(source, target) && source.Partitioning == target.Partitioning; - } - - private static bool CanUpdate(IField source, IField target) - { - return !source.IsLocked && source.Name == target.Name && source.RawProperties.TypeEquals(target.RawProperties); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs deleted file mode 100644 index f4eeabb58..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs +++ /dev/null @@ -1,150 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ExtractReferenceIds -{ - public static class ContentReferencesExtensions - { - public static IEnumerable GetReferencedIds(this IdContentData source, Schema schema, Ids strategy = Ids.All) - { - Guard.NotNull(schema, nameof(schema)); - - foreach (var field in schema.Fields) - { - var ids = source.GetReferencedIds(field, strategy); - - foreach (var id in ids) - { - yield return id; - } - } - } - - public static IEnumerable GetReferencedIds(this IdContentData source, IField field, Ids strategy = Ids.All) - { - Guard.NotNull(field, nameof(field)); - - if (source.TryGetValue(field.Id, out var fieldData)) - { - foreach (var partitionValue in fieldData) - { - var ids = field.GetReferencedIds(partitionValue.Value, strategy); - - foreach (var id in ids) - { - yield return id; - } - } - } - } - - public static IEnumerable GetReferencedIds(this NamedContentData source, Schema schema, Ids strategy = Ids.All) - { - Guard.NotNull(schema, nameof(schema)); - - return GetReferencedIds(source, schema.Fields, strategy); - } - - public static IEnumerable GetReferencedIds(this NamedContentData source, IEnumerable fields, Ids strategy = Ids.All) - { - Guard.NotNull(fields, nameof(fields)); - - foreach (var field in fields) - { - var ids = source.GetReferencedIds(field, strategy); - - foreach (var id in ids) - { - yield return id; - } - } - } - - public static IEnumerable GetReferencedIds(this NamedContentData source, IField field, Ids strategy = Ids.All) - { - Guard.NotNull(field, nameof(field)); - - if (source.TryGetValue(field.Name, out var fieldData)) - { - foreach (var partitionValue in fieldData) - { - var ids = field.GetReferencedIds(partitionValue.Value, strategy); - - foreach (var id in ids) - { - yield return id; - } - } - } - } - - public static JsonObject FormatReferences(this NamedContentData data, Schema schema, LanguagesConfig languages, string separator = ", ") - { - Guard.NotNull(schema, nameof(schema)); - - var result = JsonValue.Object(); - - foreach (var language in languages) - { - result[language.Key] = JsonValue.Create(data.FormatReferenceFields(schema, language.Key, separator)); - } - - return result; - } - - private static string FormatReferenceFields(this NamedContentData data, Schema schema, string partition, string separator) - { - Guard.NotNull(schema, nameof(schema)); - - var sb = new StringBuilder(); - - void AddValue(object value) - { - if (sb.Length > 0) - { - sb.Append(separator); - } - - sb.Append(value); - } - - var referenceFields = schema.Fields.Where(x => x.RawProperties.IsReferenceField); - - if (!referenceFields.Any()) - { - referenceFields = schema.Fields.Take(1); - } - - foreach (var referenceField in referenceFields) - { - if (data.TryGetValue(referenceField.Name, out var fieldData)) - { - if (fieldData.TryGetValue(partition, out var value)) - { - AddValue(value); - } - else if (fieldData.TryGetValue(InvariantPartitioning.Key, out var value2)) - { - AddValue(value2); - } - } - } - - return sb.ToString(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs deleted file mode 100644 index 240dffb99..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs +++ /dev/null @@ -1,106 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ExtractReferenceIds -{ - public sealed class ReferencesCleaner : IFieldVisitor - { - private readonly IJsonValue value; - private readonly ICollection oldReferences; - - private ReferencesCleaner(IJsonValue value, ICollection oldReferences) - { - this.value = value; - - this.oldReferences = oldReferences; - } - - public static IJsonValue CleanReferences(IField field, IJsonValue value, ICollection oldReferences) - { - return field.Accept(new ReferencesCleaner(value, oldReferences)); - } - - public IJsonValue Visit(IField field) - { - return CleanIds(); - } - - public IJsonValue Visit(IField field) - { - if (oldReferences.Contains(field.Properties.SingleId())) - { - return JsonValue.Array(); - } - - return CleanIds(); - } - - private IJsonValue CleanIds() - { - var ids = value.ToGuidSet(); - - var isRemoved = false; - - foreach (var oldReference in oldReferences) - { - isRemoved |= ids.Remove(oldReference); - } - - return isRemoved ? ids.ToJsonArray() : value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IArrayField field) - { - return value; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs deleted file mode 100644 index 47f032123..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ExtractReferenceIds -{ - public static class ReferencesExtensions - { - public static IEnumerable GetReferencedIds(this IField field, IJsonValue value, Ids strategy = Ids.All) - { - return ReferencesExtractor.ExtractReferences(field, value, strategy); - } - - public static IJsonValue CleanReferences(this IField field, IJsonValue value, ICollection oldReferences) - { - if (IsNull(value)) - { - return value; - } - - return ReferencesCleaner.CleanReferences(field, value, oldReferences); - } - - private static bool IsNull(IJsonValue value) - { - return value == null || value.Type == JsonValueType.Null; - } - - public static JsonArray ToJsonArray(this HashSet ids) - { - var result = JsonValue.Array(); - - foreach (var id in ids) - { - result.Add(JsonValue.Create(id.ToString())); - } - - return result; - } - - public static HashSet ToGuidSet(this IJsonValue value) - { - if (value is JsonArray array) - { - var result = new HashSet(); - - foreach (var id in array) - { - if (id.Type == JsonValueType.String && Guid.TryParse(id.ToString(), out var guid)) - { - result.Add(guid); - } - } - - return result; - } - - return new HashSet(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs deleted file mode 100644 index 90fdb52a6..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs +++ /dev/null @@ -1,116 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ExtractReferenceIds -{ - public sealed class ReferencesExtractor : IFieldVisitor> - { - private readonly IJsonValue value; - private readonly Ids strategy; - - private ReferencesExtractor(IJsonValue value, Ids strategy) - { - this.value = value; - - this.strategy = strategy; - } - - public static IEnumerable ExtractReferences(IField field, IJsonValue value, Ids strategy) - { - return field.Accept(new ReferencesExtractor(value, strategy)); - } - - public IEnumerable Visit(IArrayField field) - { - var result = new List(); - - if (value is JsonArray array) - { - foreach (var item in array.OfType()) - { - foreach (var nestedField in field.Fields) - { - if (item.TryGetValue(nestedField.Name, out var nestedValue)) - { - result.AddRange(nestedField.Accept(new ReferencesExtractor(nestedValue, strategy))); - } - } - } - } - - return result; - } - - public IEnumerable Visit(IField field) - { - var ids = value.ToGuidSet(); - - return ids; - } - - public IEnumerable Visit(IField field) - { - var ids = value.ToGuidSet(); - - if (strategy == Ids.All && field.Properties.SchemaIds != null) - { - foreach (var schemaId in field.Properties.SchemaIds) - { - ids.Add(schemaId); - } - } - - return ids; - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs deleted file mode 100644 index 55fa21cb7..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.OData.Edm; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.GenerateEdmSchema -{ - public delegate (EdmComplexType Type, bool Created) EdmTypeFactory(string names); - - public static class EdmSchemaExtensions - { - public static string EscapeEdmField(this string field) - { - return field.Replace("-", "_"); - } - - public static string UnescapeEdmField(this string field) - { - return field.Replace("_", "-"); - } - - public static EdmComplexType BuildEdmType(this Schema schema, bool withHidden, PartitionResolver partitionResolver, EdmTypeFactory typeFactory) - { - Guard.NotNull(typeFactory, nameof(typeFactory)); - Guard.NotNull(partitionResolver, nameof(partitionResolver)); - - var (edmType, _) = typeFactory("Data"); - - var visitor = new EdmTypeVisitor(typeFactory); - - foreach (var field in schema.FieldsByName.Values) - { - if (!field.IsForApi(withHidden)) - { - continue; - } - - var fieldEdmType = field.Accept(visitor); - - if (fieldEdmType == null) - { - continue; - } - - var (partitionType, created) = typeFactory($"Data.{field.Name.ToPascalCase()}"); - - if (created) - { - var partition = partitionResolver(field.Partitioning); - - foreach (var partitionItem in partition) - { - partitionType.AddStructuralProperty(partitionItem.Key.EscapeEdmField(), fieldEdmType); - } - } - - edmType.AddStructuralProperty(field.Name.EscapeEdmField(), new EdmComplexTypeReference(partitionType, false)); - } - - return edmType; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs deleted file mode 100644 index 1bcabde3a..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs +++ /dev/null @@ -1,103 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.OData.Edm; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.GenerateEdmSchema -{ - public sealed class EdmTypeVisitor : IFieldVisitor - { - private readonly EdmTypeFactory typeFactory; - - internal EdmTypeVisitor(EdmTypeFactory typeFactory) - { - this.typeFactory = typeFactory; - } - - public IEdmTypeReference CreateEdmType(IField field) - { - return field.Accept(this); - } - - public IEdmTypeReference Visit(IArrayField field) - { - var (fieldEdmType, created) = typeFactory($"Data.{field.Name.ToPascalCase()}.Item"); - - if (created) - { - foreach (var nestedField in field.Fields) - { - var nestedEdmType = nestedField.Accept(this); - - if (nestedEdmType != null) - { - fieldEdmType.AddStructuralProperty(nestedField.Name.EscapeEdmField(), nestedEdmType); - } - } - } - - return new EdmComplexTypeReference(fieldEdmType, false); - } - - public IEdmTypeReference Visit(IField field) - { - return CreatePrimitive(EdmPrimitiveTypeKind.String, field); - } - - public IEdmTypeReference Visit(IField field) - { - return CreatePrimitive(EdmPrimitiveTypeKind.Boolean, field); - } - - public IEdmTypeReference Visit(IField field) - { - return CreatePrimitive(EdmPrimitiveTypeKind.DateTimeOffset, field); - } - - public IEdmTypeReference Visit(IField field) - { - return null; - } - - public IEdmTypeReference Visit(IField field) - { - return null; - } - - public IEdmTypeReference Visit(IField field) - { - return CreatePrimitive(EdmPrimitiveTypeKind.Double, field); - } - - public IEdmTypeReference Visit(IField field) - { - return CreatePrimitive(EdmPrimitiveTypeKind.String, field); - } - - public IEdmTypeReference Visit(IField field) - { - return CreatePrimitive(EdmPrimitiveTypeKind.String, field); - } - - public IEdmTypeReference Visit(IField field) - { - return CreatePrimitive(EdmPrimitiveTypeKind.String, field); - } - - public IEdmTypeReference Visit(IField field) - { - return null; - } - - private static IEdmTypeReference CreatePrimitive(EdmPrimitiveTypeKind kind, IField field) - { - return EdmCoreModel.Instance.GetPrimitive(kind, !field.RawProperties.IsRequired); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs deleted file mode 100644 index f7f5fcd7b..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NJsonSchema; - -namespace Squidex.Domain.Apps.Core.GenerateJsonSchema -{ - public static class Builder - { - public static JsonSchema Object() - { - return new JsonSchema { Type = JsonObjectType.Object }; - } - - public static JsonSchema Guid() - { - return new JsonSchema { Type = JsonObjectType.String, Format = JsonFormatStrings.Guid }; - } - - public static JsonSchema String() - { - return new JsonSchema { Type = JsonObjectType.String }; - } - - public static JsonSchemaProperty ArrayProperty(JsonSchema item) - { - return new JsonSchemaProperty { Type = JsonObjectType.Array, Item = item }; - } - - public static JsonSchemaProperty BooleanProperty() - { - return new JsonSchemaProperty { Type = JsonObjectType.Boolean }; - } - - public static JsonSchemaProperty DateTimeProperty(string description = null, bool isRequired = false) - { - return new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime, Description = description, IsRequired = isRequired }; - } - - public static JsonSchemaProperty GuidProperty(string description = null, bool isRequired = false) - { - return new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.Guid, Description = description, IsRequired = isRequired }; - } - - public static JsonSchemaProperty NumberProperty(string description = null, bool isRequired = false) - { - return new JsonSchemaProperty { Type = JsonObjectType.Number, Description = description, IsRequired = isRequired }; - } - - public static JsonSchemaProperty ObjectProperty(JsonSchema item, string description = null, bool isRequired = false) - { - return new JsonSchemaProperty { Type = JsonObjectType.Object, Reference = item, Description = description, IsRequired = isRequired }; - } - - public static JsonSchemaProperty StringProperty(string description = null, bool isRequired = false) - { - return new JsonSchemaProperty { Type = JsonObjectType.String, Description = description, IsRequired = isRequired }; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs deleted file mode 100644 index 878591922..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NJsonSchema; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.GenerateJsonSchema -{ - public sealed class ContentSchemaBuilder - { - public JsonSchema CreateContentSchema(Schema schema, JsonSchema dataSchema) - { - Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(dataSchema, nameof(dataSchema)); - - var schemaName = schema.Properties.Label.WithFallback(schema.Name); - - var contentSchema = new JsonSchema - { - Properties = - { - ["id"] = Builder.GuidProperty($"The id of the {schemaName} content.", true), - ["data"] = Builder.ObjectProperty(dataSchema, $"The data of the {schemaName}.", true), - ["dataDraft"] = Builder.ObjectProperty(dataSchema, $"The draft data of the {schemaName}.", false), - ["version"] = Builder.NumberProperty($"The version of the {schemaName}.", true), - ["created"] = Builder.DateTimeProperty($"The date and time when the {schemaName} content has been created.", true), - ["createdBy"] = Builder.StringProperty($"The user that has created the {schemaName} content.", true), - ["lastModified"] = Builder.DateTimeProperty($"The date and time when the {schemaName} content has been modified last.", true), - ["lastModifiedBy"] = Builder.StringProperty($"The user that has updated the {schemaName} content last.", true), - ["status"] = Builder.StringProperty($"The status of the content.", true) - }, - Type = JsonObjectType.Object - }; - - return contentSchema; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs deleted file mode 100644 index ef1243f3e..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NJsonSchema; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.GenerateJsonSchema -{ - public static class JsonSchemaExtensions - { - public static JsonSchema BuildJsonSchema(this Schema schema, PartitionResolver partitionResolver, SchemaResolver schemaResolver, bool withHidden = false) - { - Guard.NotNull(schemaResolver, nameof(schemaResolver)); - Guard.NotNull(partitionResolver, nameof(partitionResolver)); - - var schemaName = schema.Name.ToPascalCase(); - - var jsonTypeVisitor = new JsonTypeVisitor(schemaResolver, withHidden); - var jsonSchema = Builder.Object(); - - foreach (var field in schema.Fields.ForApi(withHidden)) - { - var partitionObject = Builder.Object(); - var partitionSet = partitionResolver(field.Partitioning); - - foreach (var partitionItem in partitionSet) - { - var partitionItemProperty = field.Accept(jsonTypeVisitor); - - if (partitionItemProperty != null) - { - partitionItemProperty.Description = partitionItem.Name; - partitionItemProperty.IsRequired = field.RawProperties.IsRequired && !partitionItem.IsOptional; - - partitionObject.Properties.Add(partitionItem.Key, partitionItemProperty); - } - } - - if (partitionObject.Properties.Count > 0) - { - var propertyReference = schemaResolver($"{schemaName}{field.Name.ToPascalCase()}Property", partitionObject); - - jsonSchema.Properties.Add(field.Name, CreateProperty(field, propertyReference)); - } - } - - return jsonSchema; - } - - public static JsonSchemaProperty CreateProperty(IField field, JsonSchema reference) - { - var jsonProperty = Builder.ObjectProperty(reference); - - if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints)) - { - jsonProperty.Description = $"{field.Name} ({field.RawProperties.Hints})"; - } - else - { - jsonProperty.Description = field.Name; - } - - jsonProperty.IsRequired = field.RawProperties.IsRequired; - - return jsonProperty; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs deleted file mode 100644 index f83c3ceb1..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs +++ /dev/null @@ -1,151 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.ObjectModel; -using NJsonSchema; -using Squidex.Domain.Apps.Core.Schemas; - -namespace Squidex.Domain.Apps.Core.GenerateJsonSchema -{ - public delegate JsonSchema SchemaResolver(string name, JsonSchema schema); - - public sealed class JsonTypeVisitor : IFieldVisitor - { - private readonly SchemaResolver schemaResolver; - private readonly bool withHiddenFields; - - public JsonTypeVisitor(SchemaResolver schemaResolver, bool withHiddenFields) - { - this.schemaResolver = schemaResolver; - - this.withHiddenFields = withHiddenFields; - } - - public JsonSchemaProperty Visit(IArrayField field) - { - var item = Builder.Object(); - - foreach (var nestedField in field.Fields.ForApi(withHiddenFields)) - { - var childProperty = nestedField.Accept(this); - - if (childProperty != null) - { - childProperty.Description = nestedField.RawProperties.Hints; - childProperty.IsRequired = nestedField.RawProperties.IsRequired; - - item.Properties.Add(nestedField.Name, childProperty); - } - } - - return Builder.ArrayProperty(item); - } - - public JsonSchemaProperty Visit(IField field) - { - var item = schemaResolver("AssetItem", Builder.Guid()); - - return Builder.ArrayProperty(item); - } - - public JsonSchemaProperty Visit(IField field) - { - return Builder.BooleanProperty(); - } - - public JsonSchemaProperty Visit(IField field) - { - return Builder.DateTimeProperty(); - } - - public JsonSchemaProperty Visit(IField field) - { - var geolocationSchema = Builder.Object(); - - geolocationSchema.Properties.Add("latitude", new JsonSchemaProperty - { - Type = JsonObjectType.Number, - Minimum = -90, - Maximum = 90, - IsRequired = true - }); - - geolocationSchema.Properties.Add("longitude", new JsonSchemaProperty - { - Type = JsonObjectType.Number, - Minimum = -180, - Maximum = 180, - IsRequired = true - }); - - var reference = schemaResolver("GeolocationDto", geolocationSchema); - - return Builder.ObjectProperty(reference); - } - - public JsonSchemaProperty Visit(IField field) - { - return Builder.StringProperty(); - } - - public JsonSchemaProperty Visit(IField field) - { - var property = Builder.NumberProperty(); - - if (field.Properties.MinValue.HasValue) - { - property.Minimum = (decimal)field.Properties.MinValue.Value; - } - - if (field.Properties.MaxValue.HasValue) - { - property.Maximum = (decimal)field.Properties.MaxValue.Value; - } - - return property; - } - - public JsonSchemaProperty Visit(IField field) - { - var item = schemaResolver("ReferenceItem", Builder.Guid()); - - return Builder.ArrayProperty(item); - } - - public JsonSchemaProperty Visit(IField field) - { - var property = Builder.StringProperty(); - - property.MinLength = field.Properties.MinLength; - property.MaxLength = field.Properties.MaxLength; - - if (field.Properties.AllowedValues != null) - { - var names = property.EnumerationNames = property.EnumerationNames ?? new Collection(); - - foreach (var value in field.Properties.AllowedValues) - { - names.Add(value); - } - } - - return property; - } - - public JsonSchemaProperty Visit(IField field) - { - var item = schemaResolver("ReferenceItem", Builder.String()); - - return Builder.ArrayProperty(item); - } - - public JsonSchemaProperty Visit(IField field) - { - return null; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs deleted file mode 100644 index e21540f2d..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Runtime.Serialization; -using Squidex.Infrastructure; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents -{ - public abstract class EnrichedUserEventBase : EnrichedEvent - { - public RefToken Actor { get; set; } - - [IgnoreDataMember] - public IUser User { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs deleted file mode 100644 index b8515b1c5..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs +++ /dev/null @@ -1,78 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public sealed class EventEnricher : IEventEnricher - { - private static readonly TimeSpan UserCacheDuration = TimeSpan.FromMinutes(10); - private readonly IMemoryCache userCache; - private readonly IUserResolver userResolver; - - public EventEnricher(IMemoryCache userCache, IUserResolver userResolver) - { - Guard.NotNull(userCache, nameof(userCache)); - Guard.NotNull(userResolver, nameof(userResolver)); - - this.userCache = userCache; - this.userResolver = userResolver; - } - - public async Task EnrichAsync(EnrichedEvent enrichedEvent, Envelope @event) - { - enrichedEvent.Timestamp = @event.Headers.Timestamp(); - - if (enrichedEvent is EnrichedUserEventBase userEvent) - { - if (@event.Payload is SquidexEvent squidexEvent) - { - userEvent.Actor = squidexEvent.Actor; - } - - userEvent.User = await FindUserAsync(userEvent.Actor); - } - - enrichedEvent.AppId = @event.Payload.AppId; - } - - private Task FindUserAsync(RefToken actor) - { - var key = $"EventEnrichers_Users_${actor.Identifier}"; - - return userCache.GetOrCreateAsync(key, async x => - { - x.AbsoluteExpirationRelativeToNow = UserCacheDuration; - - IUser user; - try - { - user = await userResolver.FindByIdOrEmailAsync(actor.Identifier); - } - catch - { - user = null; - } - - if (user == null && actor.IsClient) - { - user = new ClientUser(actor); - } - - return user; - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs deleted file mode 100644 index b7cd99f60..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public interface IRuleTriggerHandler - { - Type TriggerType { get; } - - Task CreateEnrichedEventAsync(Envelope @event); - - bool Trigger(EnrichedEvent @event, RuleTrigger trigger); - - bool Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId); - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs deleted file mode 100644 index d73750a63..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Text; - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public sealed class Result - { - public Exception Exception { get; private set; } - - public string Dump { get; private set; } - - public RuleResult Status { get; private set; } - - public void Enrich(TimeSpan elapsed) - { - var dumpBuilder = new StringBuilder(); - - if (!string.IsNullOrWhiteSpace(Dump)) - { - dumpBuilder.AppendLine(Dump); - } - - if (Status == RuleResult.Timeout) - { - dumpBuilder.AppendLine(); - dumpBuilder.AppendLine("Action timed out."); - } - - dumpBuilder.AppendLine(); - dumpBuilder.AppendFormat("Elapsed {0}.", elapsed); - dumpBuilder.AppendLine(); - - Dump = dumpBuilder.ToString(); - } - - public static Result Ignored() - { - return Success("Ignored"); - } - - public static Result Complete() - { - return Success("Completed"); - } - - public static Result Create(string dump, RuleResult result) - { - return new Result { Dump = dump, Status = result }; - } - - public static Result Success(string dump) - { - return new Result { Dump = dump, Status = RuleResult.Success }; - } - - public static Result Failed(Exception ex) - { - return Failed(ex, ex?.Message); - } - - public static Result SuccessOrFailed(Exception ex, string dump) - { - if (ex != null) - { - return Failed(ex, dump); - } - else - { - return Success(dump); - } - } - - public static Result Failed(Exception ex, string dump) - { - var result = new Result { Exception = ex, Dump = dump ?? ex.Message }; - - if (ex is OperationCanceledException || ex is TimeoutException) - { - result.Status = RuleResult.Timeout; - } - else - { - result.Status = RuleResult.Failed; - } - - return result; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs deleted file mode 100644 index cbc5d7698..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Infrastructure; - -#pragma warning disable RECS0083 // Shows NotImplementedException throws in the quick task bar - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public abstract class RuleActionHandler : IRuleActionHandler where TAction : RuleAction - { - private readonly RuleEventFormatter formatter; - - Type IRuleActionHandler.ActionType - { - get { return typeof(TAction); } - } - - Type IRuleActionHandler.DataType - { - get { return typeof(TData); } - } - - protected RuleActionHandler(RuleEventFormatter formatter) - { - Guard.NotNull(formatter, nameof(formatter)); - - this.formatter = formatter; - } - - protected virtual string ToJson(T @event) - { - return formatter.ToPayload(@event); - } - - protected virtual string ToEnvelopeJson(EnrichedEvent @event) - { - return formatter.ToEnvelope(@event); - } - - protected string Format(Uri uri, EnrichedEvent @event) - { - return formatter.Format(uri.ToString(), @event); - } - - protected string Format(string text, EnrichedEvent @event) - { - return formatter.Format(text, @event); - } - - async Task<(string Description, object Data)> IRuleActionHandler.CreateJobAsync(EnrichedEvent @event, RuleAction action) - { - var (description, data) = await CreateJobAsync(@event, (TAction)action); - - return (description, data); - } - - async Task IRuleActionHandler.ExecuteJobAsync(object data, CancellationToken ct) - { - var typedData = (TData)data; - - return await ExecuteJobAsync(typedData, ct); - } - - protected virtual Task<(string Description, TData Data)> CreateJobAsync(EnrichedEvent @event, TAction action) - { - return Task.FromResult(CreateJob(@event, action)); - } - - protected virtual (string Description, TData Data) CreateJob(EnrichedEvent @event, TAction action) - { - throw new NotImplementedException(); - } - - protected abstract Task ExecuteJobAsync(TData job, CancellationToken ct = default); - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs deleted file mode 100644 index 1611e76ea..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public sealed class RuleActionProperty - { - public RuleActionPropertyEditor Editor { get; set; } - - public string Name { get; set; } - - public string Display { get; set; } - - public string Description { get; set; } - - public bool IsFormattable { get; set; } - - public bool IsRequired { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs deleted file mode 100644 index 2d0477228..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public sealed class RuleActionRegistration - { - public Type ActionType { get; } - - internal RuleActionRegistration(Type actionType) - { - Guard.NotNull(actionType, nameof(actionType)); - - ActionType = actionType; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs deleted file mode 100644 index 6aeb22e82..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ /dev/null @@ -1,314 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// =========================================-================================= - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text; -using System.Text.RegularExpressions; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public class RuleEventFormatter - { - private const string Fallback = "null"; - private const string ScriptSuffix = ")"; - private const string ScriptPrefix = "Script("; - private static readonly char[] ContentPlaceholderStartOld = "CONTENT_DATA".ToCharArray(); - private static readonly char[] ContentPlaceholderStartNew = "{CONTENT_DATA".ToCharArray(); - private static readonly Regex ContentDataPlaceholderOld = new Regex(@"^CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled); - private static readonly Regex ContentDataPlaceholderNew = new Regex(@"^\{CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}\}", RegexOptions.Compiled); - private readonly List<(char[] Pattern, Func Replacer)> patterns = new List<(char[] Pattern, Func Replacer)>(); - private readonly IJsonSerializer jsonSerializer; - private readonly IRuleUrlGenerator urlGenerator; - private readonly IScriptEngine scriptEngine; - - public RuleEventFormatter(IJsonSerializer jsonSerializer, IRuleUrlGenerator urlGenerator, IScriptEngine scriptEngine) - { - Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(urlGenerator, nameof(urlGenerator)); - - this.jsonSerializer = jsonSerializer; - this.scriptEngine = scriptEngine; - this.urlGenerator = urlGenerator; - - AddPattern("APP_ID", AppId); - AddPattern("APP_NAME", AppName); - AddPattern("CONTENT_ACTION", ContentAction); - AddPattern("CONTENT_STATUS", ContentStatus); - AddPattern("CONTENT_URL", ContentUrl); - AddPattern("SCHEMA_ID", SchemaId); - AddPattern("SCHEMA_NAME", SchemaName); - AddPattern("TIMESTAMP_DATETIME", TimestampTime); - AddPattern("TIMESTAMP_DATE", TimestampDate); - AddPattern("USER_ID", UserId); - AddPattern("USER_NAME", UserName); - AddPattern("USER_EMAIL", UserEmail); - } - - private void AddPattern(string placeholder, Func generator) - { - patterns.Add((placeholder.ToCharArray(), generator)); - } - - public virtual string ToPayload(T @event) - { - return jsonSerializer.Serialize(@event); - } - - public virtual string ToEnvelope(EnrichedEvent @event) - { - return jsonSerializer.Serialize(new { type = @event.Name, payload = @event, timestamp = @event.Timestamp }); - } - - public string Format(string text, EnrichedEvent @event) - { - if (string.IsNullOrWhiteSpace(text)) - { - return text; - } - - var trimmed = text.Trim(); - - if (trimmed.StartsWith(ScriptPrefix, StringComparison.OrdinalIgnoreCase) && trimmed.EndsWith(ScriptSuffix, StringComparison.OrdinalIgnoreCase)) - { - var script = trimmed.Substring(ScriptPrefix.Length, trimmed.Length - ScriptPrefix.Length - ScriptSuffix.Length); - - var customFunctions = new Dictionary> - { - ["contentUrl"] = () => ContentUrl(@event), - ["contentAction"] = () => ContentAction(@event) - }; - - return scriptEngine.Interpolate("event", @event, script, customFunctions); - } - - var current = text.AsSpan(); - - var sb = new StringBuilder(); - - var cp2 = new ReadOnlySpan(ContentPlaceholderStartNew); - var cp1 = new ReadOnlySpan(ContentPlaceholderStartOld); - - for (var i = 0; i < current.Length; i++) - { - var c = current[i]; - - if (c == '$') - { - sb.Append(current.Slice(0, i).ToString()); - - current = current.Slice(i); - - var test = current.Slice(1); - var tested = false; - - for (var j = 0; j < patterns.Count; j++) - { - var (pattern, replacer) = patterns[j]; - - if (test.StartsWith(pattern, StringComparison.OrdinalIgnoreCase)) - { - sb.Append(replacer(@event)); - - current = current.Slice(pattern.Length + 1); - i = 0; - - tested = true; - break; - } - } - - if (!tested && (test.StartsWith(cp1, StringComparison.OrdinalIgnoreCase) || test.StartsWith(cp2, StringComparison.OrdinalIgnoreCase))) - { - var currentString = test.ToString(); - - var match = ContentDataPlaceholderOld.Match(currentString); - - if (!match.Success) - { - match = ContentDataPlaceholderNew.Match(currentString); - } - - if (match.Success) - { - if (@event is EnrichedContentEvent contentEvent) - { - sb.Append(CalculateData(contentEvent.Data, match)); - } - else - { - sb.Append(Fallback); - } - - current = current.Slice(match.Length + 1); - i = 0; - } - } - } - } - - sb.Append(current.ToString()); - - return sb.ToString(); - } - - private static string TimestampDate(EnrichedEvent @event) - { - return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd", CultureInfo.InvariantCulture); - } - - private static string TimestampTime(EnrichedEvent @event) - { - return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd-hh-mm-ss", CultureInfo.InvariantCulture); - } - - private static string AppId(EnrichedEvent @event) - { - return @event.AppId.Id.ToString(); - } - - private static string AppName(EnrichedEvent @event) - { - return @event.AppId.Name; - } - - private static string SchemaId(EnrichedEvent @event) - { - if (@event is EnrichedSchemaEventBase schemaEvent) - { - return schemaEvent.SchemaId.Id.ToString(); - } - - return Fallback; - } - - private static string SchemaName(EnrichedEvent @event) - { - if (@event is EnrichedSchemaEventBase schemaEvent) - { - return schemaEvent.SchemaId.Name; - } - - return Fallback; - } - - private static string ContentAction(EnrichedEvent @event) - { - if (@event is EnrichedContentEvent contentEvent) - { - return contentEvent.Type.ToString(); - } - - return Fallback; - } - - private static string ContentStatus(EnrichedEvent @event) - { - if (@event is EnrichedContentEvent contentEvent) - { - return contentEvent.Status.ToString(); - } - - return Fallback; - } - - private string ContentUrl(EnrichedEvent @event) - { - if (@event is EnrichedContentEvent contentEvent) - { - return urlGenerator.GenerateContentUIUrl(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); - } - - return Fallback; - } - - private static string UserName(EnrichedEvent @event) - { - if (@event is EnrichedUserEventBase userEvent) - { - return userEvent.User?.DisplayName() ?? Fallback; - } - - return Fallback; - } - - private static string UserId(EnrichedEvent @event) - { - if (@event is EnrichedUserEventBase userEvent) - { - return userEvent.User?.Id ?? Fallback; - } - - return Fallback; - } - - private static string UserEmail(EnrichedEvent @event) - { - if (@event is EnrichedUserEventBase userEvent) - { - return userEvent.User?.Email ?? Fallback; - } - - return Fallback; - } - - private static string CalculateData(NamedContentData data, Match match) - { - var captures = match.Groups[2].Captures; - - var path = new string[captures.Count]; - - for (var i = 0; i < path.Length; i++) - { - path[i] = captures[i].Value; - } - - if (!data.TryGetValue(path[0], out var field)) - { - return Fallback; - } - - if (!field.TryGetValue(path[1], out var value)) - { - return Fallback; - } - - for (var j = 2; j < path.Length; j++) - { - if (value is JsonObject obj && obj.TryGetValue(path[j], out value)) - { - continue; - } - - if (value is JsonArray array && int.TryParse(path[j], out var idx) && idx >= 0 && idx < array.Count) - { - value = array[idx]; - } - else - { - return Fallback; - } - } - - if (value == null || value.Type == JsonValueType.Null) - { - return Fallback; - } - - return value.ToString() ?? Fallback; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs deleted file mode 100644 index 8666a5206..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs +++ /dev/null @@ -1,189 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Reflection; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Reflection; - -#pragma warning disable RECS0033 // Convert 'if' to '||' expression - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public sealed class RuleRegistry : ITypeProvider - { - private const string ActionSuffix = "Action"; - private const string ActionSuffixV2 = "ActionV2"; - private readonly Dictionary actionTypes = new Dictionary(); - - public IReadOnlyDictionary Actions - { - get { return actionTypes; } - } - - public RuleRegistry(IEnumerable registrations = null) - { - if (registrations != null) - { - foreach (var registration in registrations) - { - Add(registration.ActionType); - } - } - } - - public void Add() where T : RuleAction - { - Add(typeof(T)); - } - - private void Add(Type actionType) - { - var metadata = actionType.GetCustomAttribute(); - - if (metadata == null) - { - return; - } - - var name = GetActionName(actionType); - - var definition = - new RuleActionDefinition - { - Type = actionType, - Title = metadata.Title, - Display = metadata.Display, - Description = metadata.Description, - IconColor = metadata.IconColor, - IconImage = metadata.IconImage, - ReadMore = metadata.ReadMore - }; - - foreach (var property in actionType.GetProperties()) - { - if (property.CanRead && property.CanWrite) - { - var actionProperty = new RuleActionProperty { Name = property.Name.ToCamelCase(), Display = property.Name }; - - var display = property.GetCustomAttribute(); - - if (!string.IsNullOrWhiteSpace(display?.Name)) - { - actionProperty.Display = display.Name; - } - - if (!string.IsNullOrWhiteSpace(display?.Description)) - { - actionProperty.Description = display.Description; - } - - var type = property.PropertyType; - - if ((GetDataAttribute(property) != null || (type.IsValueType && !IsNullable(type))) && type != typeof(bool) && type != typeof(bool?)) - { - actionProperty.IsRequired = true; - } - - if (property.GetCustomAttribute() != null) - { - actionProperty.IsFormattable = true; - } - - var dataType = GetDataAttribute(property)?.DataType; - - if (type == typeof(bool) || type == typeof(bool?)) - { - actionProperty.Editor = RuleActionPropertyEditor.Checkbox; - } - else if (type == typeof(int) || type == typeof(int?)) - { - actionProperty.Editor = RuleActionPropertyEditor.Number; - } - else if (dataType == DataType.Url) - { - actionProperty.Editor = RuleActionPropertyEditor.Url; - } - else if (dataType == DataType.Password) - { - actionProperty.Editor = RuleActionPropertyEditor.Password; - } - else if (dataType == DataType.EmailAddress) - { - actionProperty.Editor = RuleActionPropertyEditor.Email; - } - else if (dataType == DataType.MultilineText) - { - actionProperty.Editor = RuleActionPropertyEditor.TextArea; - } - else - { - actionProperty.Editor = RuleActionPropertyEditor.Text; - } - - definition.Properties.Add(actionProperty); - } - } - - actionTypes[name] = definition; - } - - private static T GetDataAttribute(PropertyInfo property) where T : ValidationAttribute - { - var result = property.GetCustomAttribute(); - - result?.IsValid(null); - - return result; - } - - private static bool IsNullable(Type type) - { - return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); - } - - private static string GetActionName(Type type) - { - return type.TypeName(false, ActionSuffix, ActionSuffixV2); - } - - public void Map(TypeNameRegistry typeNameRegistry) - { - foreach (var actionType in actionTypes.Values) - { - typeNameRegistry.Map(actionType.Type, actionType.Type.Name); - } - - var eventTypes = typeof(EnrichedEvent).Assembly.GetTypes().Where(x => typeof(EnrichedEvent).IsAssignableFrom(x) && !x.IsAbstract); - - var addedTypes = new HashSet(); - - foreach (var type in eventTypes) - { - if (addedTypes.Add(type)) - { - typeNameRegistry.Map(type, type.Name); - } - } - - var triggerTypes = typeof(RuleTrigger).Assembly.GetTypes().Where(x => typeof(RuleTrigger).IsAssignableFrom(x) && !x.IsAbstract); - - foreach (var type in triggerTypes) - { - if (addedTypes.Add(type)) - { - typeNameRegistry.Map(type, type.Name); - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs deleted file mode 100644 index 21319f7de..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ /dev/null @@ -1,202 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using NodaTime; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public class RuleService - { - private readonly Dictionary ruleActionHandlers; - private readonly Dictionary ruleTriggerHandlers; - private readonly TypeNameRegistry typeNameRegistry; - private readonly RuleOptions ruleOptions; - private readonly IEventEnricher eventEnricher; - private readonly IJsonSerializer jsonSerializer; - private readonly IClock clock; - private readonly ISemanticLog log; - - public RuleService( - IOptions ruleOptions, - IEnumerable ruleTriggerHandlers, - IEnumerable ruleActionHandlers, - IEventEnricher eventEnricher, - IJsonSerializer jsonSerializer, - IClock clock, - ISemanticLog log, - TypeNameRegistry typeNameRegistry) - { - Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); - Guard.NotNull(ruleOptions, nameof(ruleOptions)); - Guard.NotNull(ruleTriggerHandlers, nameof(ruleTriggerHandlers)); - Guard.NotNull(ruleActionHandlers, nameof(ruleActionHandlers)); - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); - Guard.NotNull(eventEnricher, nameof(eventEnricher)); - Guard.NotNull(clock, nameof(clock)); - Guard.NotNull(log, nameof(log)); - - this.typeNameRegistry = typeNameRegistry; - - this.ruleOptions = ruleOptions.Value; - this.ruleTriggerHandlers = ruleTriggerHandlers.ToDictionary(x => x.TriggerType); - this.ruleActionHandlers = ruleActionHandlers.ToDictionary(x => x.ActionType); - this.eventEnricher = eventEnricher; - - this.jsonSerializer = jsonSerializer; - - this.clock = clock; - - this.log = log; - } - - public virtual async Task CreateJobAsync(Rule rule, Guid ruleId, Envelope @event) - { - Guard.NotNull(rule, nameof(rule)); - Guard.NotNull(@event, nameof(@event)); - - try - { - if (!rule.IsEnabled) - { - return null; - } - - if (!(@event.Payload is AppEvent)) - { - return null; - } - - var typed = @event.To(); - - var actionType = rule.Action.GetType(); - - if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) - { - return null; - } - - if (!ruleActionHandlers.TryGetValue(actionType, out var actionHandler)) - { - return null; - } - - var now = clock.GetCurrentInstant(); - - var eventTime = - @event.Headers.ContainsKey(CommonHeaders.Timestamp) ? - @event.Headers.Timestamp() : - now; - - var expires = eventTime.Plus(Constants.ExpirationTime); - - if (eventTime.Plus(Constants.StaleTime) < now) - { - return null; - } - - if (!triggerHandler.Trigger(typed.Payload, rule.Trigger, ruleId)) - { - return null; - } - - var appEventEnvelope = @event.To(); - - var enrichedEvent = await triggerHandler.CreateEnrichedEventAsync(appEventEnvelope); - - if (enrichedEvent == null) - { - return null; - } - - await eventEnricher.EnrichAsync(enrichedEvent, typed); - - if (!triggerHandler.Trigger(enrichedEvent, rule.Trigger)) - { - return null; - } - - var actionName = typeNameRegistry.GetName(actionType); - var actionData = await actionHandler.CreateJobAsync(enrichedEvent, rule.Action); - - var json = jsonSerializer.Serialize(actionData.Data); - - var job = new RuleJob - { - Id = Guid.NewGuid(), - ActionData = json, - ActionName = actionName, - AppId = enrichedEvent.AppId.Id, - Created = now, - Description = actionData.Description, - EventName = enrichedEvent.Name, - ExecutionPartition = enrichedEvent.Partition, - Expires = expires, - RuleId = ruleId - }; - - return job; - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "createRuleJob") - .WriteProperty("status", "Failed")); - - return null; - } - } - - public virtual async Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job) - { - var actionWatch = ValueStopwatch.StartNew(); - - Result result; - - try - { - var actionType = typeNameRegistry.GetType(actionName); - var actionHandler = ruleActionHandlers[actionType]; - - var deserialized = jsonSerializer.Deserialize(job, actionHandler.DataType); - - using (var cts = new CancellationTokenSource(GetTimeoutInMs())) - { - result = await actionHandler.ExecuteJobAsync(deserialized, cts.Token).WithCancellation(cts.Token); - } - } - catch (Exception ex) - { - result = Result.Failed(ex); - } - - var elapsed = TimeSpan.FromMilliseconds(actionWatch.Stop()); - - result.Enrich(elapsed); - - return (result, elapsed); - } - - private int GetTimeoutInMs() - { - return ruleOptions.ExecutionTimeoutInSeconds * 1000; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs deleted file mode 100644 index c369497ac..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure.EventSourcing; - -#pragma warning disable IDE0019 // Use pattern matching - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public abstract class RuleTriggerHandler : IRuleTriggerHandler - where TTrigger : RuleTrigger - where TEvent : AppEvent - where TEnrichedEvent : EnrichedEvent - { - public Type TriggerType - { - get { return typeof(TTrigger); } - } - - async Task IRuleTriggerHandler.CreateEnrichedEventAsync(Envelope @event) - { - return await CreateEnrichedEventAsync(@event.To()); - } - - bool IRuleTriggerHandler.Trigger(EnrichedEvent @event, RuleTrigger trigger) - { - if (@event is TEnrichedEvent typed) - { - return Trigger(typed, (TTrigger)trigger); - } - - return false; - } - - bool IRuleTriggerHandler.Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId) - { - if (@event is TEvent typed) - { - return Trigger(typed, (TTrigger)trigger, ruleId); - } - - return false; - } - - protected abstract Task CreateEnrichedEventAsync(Envelope @event); - - protected abstract bool Trigger(TEnrichedEvent @event, TTrigger trigger); - - protected virtual bool Trigger(TEvent @event, TTrigger trigger, Guid ruleId) - { - return true; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs deleted file mode 100644 index e75e1f0b8..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs +++ /dev/null @@ -1,130 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Jint; -using Jint.Native; -using Jint.Native.Object; -using Jint.Runtime.Descriptors; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; - -#pragma warning disable RECS0133 // Parameter name differs in base declaration - -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper -{ - public sealed class ContentDataObject : ObjectInstance - { - private readonly NamedContentData contentData; - private HashSet fieldsToDelete; - private Dictionary fieldProperties; - private bool isChanged; - - public ContentDataObject(Engine engine, NamedContentData contentData) - : base(engine) - { - Extensible = true; - - this.contentData = contentData; - } - - public void MarkChanged() - { - isChanged = true; - } - - public bool TryUpdate(out NamedContentData result) - { - result = contentData; - - if (isChanged) - { - if (fieldsToDelete != null) - { - foreach (var field in fieldsToDelete) - { - contentData.Remove(field); - } - } - - if (fieldProperties != null) - { - foreach (var kvp in fieldProperties) - { - var value = (ContentDataProperty)kvp.Value; - - if (value.ContentField.TryUpdate(out var fieldData)) - { - contentData[kvp.Key] = fieldData; - } - } - } - } - - return isChanged; - } - - public override void RemoveOwnProperty(string propertyName) - { - if (fieldsToDelete == null) - { - fieldsToDelete = new HashSet(); - } - - fieldsToDelete.Add(propertyName); - fieldProperties?.Remove(propertyName); - - MarkChanged(); - } - - public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError) - { - EnsurePropertiesInitialized(); - - if (!fieldProperties.ContainsKey(propertyName)) - { - fieldProperties[propertyName] = new ContentDataProperty(this) { Value = desc.Value }; - } - - return true; - } - - public override void Put(string propertyName, JsValue value, bool throwOnError) - { - EnsurePropertiesInitialized(); - - fieldProperties.GetOrAdd(propertyName, this, (k, c) => new ContentDataProperty(c)).Value = value; - } - - public override PropertyDescriptor GetOwnProperty(string propertyName) - { - EnsurePropertiesInitialized(); - - return fieldProperties.GetOrAdd(propertyName, this, (k, c) => new ContentDataProperty(c, new ContentFieldObject(c, new ContentFieldData(), false))); - } - - public override IEnumerable> GetOwnProperties() - { - EnsurePropertiesInitialized(); - - return fieldProperties; - } - - private void EnsurePropertiesInitialized() - { - if (fieldProperties == null) - { - fieldProperties = new Dictionary(contentData.Count); - - foreach (var kvp in contentData) - { - fieldProperties.Add(kvp.Key, new ContentDataProperty(this, new ContentFieldObject(this, kvp.Value, false))); - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs deleted file mode 100644 index b9a51cfbf..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Jint.Native; -using Jint.Runtime; -using Squidex.Domain.Apps.Core.Contents; - -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper -{ - public sealed class ContentDataProperty : CustomProperty - { - private readonly ContentDataObject contentData; - private ContentFieldObject contentField; - private JsValue value; - - protected override JsValue CustomValue - { - get - { - return value; - } - set - { - if (!Equals(this.value, value)) - { - if (value == null || !value.IsObject()) - { - throw new JavaScriptException("You can only assign objects to content data."); - } - - var obj = value.AsObject(); - - contentField = new ContentFieldObject(contentData, new ContentFieldData(), true); - - foreach (var kvp in obj.GetOwnProperties()) - { - contentField.Put(kvp.Key, kvp.Value.Value, true); - } - - this.value = contentField; - } - } - } - - public ContentFieldObject ContentField - { - get { return contentField; } - } - - public ContentDataProperty(ContentDataObject contentData, ContentFieldObject contentField = null) - { - this.contentData = contentData; - this.contentField = contentField; - - if (contentField != null) - { - value = contentField; - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs deleted file mode 100644 index d6ef06266..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs +++ /dev/null @@ -1,135 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Jint.Native.Object; -using Jint.Runtime.Descriptors; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; - -#pragma warning disable RECS0133 // Parameter name differs in base declaration - -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper -{ - public sealed class ContentFieldObject : ObjectInstance - { - private readonly ContentDataObject contentData; - private readonly ContentFieldData fieldData; - private HashSet valuesToDelete; - private Dictionary valueProperties; - private bool isChanged; - - public ContentFieldData FieldData - { - get { return fieldData; } - } - - public ContentFieldObject(ContentDataObject contentData, ContentFieldData fieldData, bool isNew) - : base(contentData.Engine) - { - Extensible = true; - - this.contentData = contentData; - this.fieldData = fieldData; - - if (isNew) - { - MarkChanged(); - } - } - - public void MarkChanged() - { - isChanged = true; - - contentData.MarkChanged(); - } - - public bool TryUpdate(out ContentFieldData result) - { - result = fieldData; - - if (isChanged) - { - if (valuesToDelete != null) - { - foreach (var field in valuesToDelete) - { - fieldData.Remove(field); - } - } - - if (valueProperties != null) - { - foreach (var kvp in valueProperties) - { - var value = (ContentFieldProperty)kvp.Value; - - if (value.IsChanged) - { - fieldData[kvp.Key] = value.ContentValue; - } - } - } - } - - return isChanged; - } - - public override void RemoveOwnProperty(string propertyName) - { - if (valuesToDelete == null) - { - valuesToDelete = new HashSet(); - } - - valuesToDelete.Add(propertyName); - valueProperties?.Remove(propertyName); - - MarkChanged(); - } - - public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError) - { - EnsurePropertiesInitialized(); - - if (!valueProperties.ContainsKey(propertyName)) - { - valueProperties[propertyName] = new ContentFieldProperty(this) { Value = desc.Value }; - } - - return true; - } - - public override PropertyDescriptor GetOwnProperty(string propertyName) - { - EnsurePropertiesInitialized(); - - return valueProperties?.GetOrDefault(propertyName) ?? PropertyDescriptor.Undefined; - } - - public override IEnumerable> GetOwnProperties() - { - EnsurePropertiesInitialized(); - - return valueProperties; - } - - private void EnsurePropertiesInitialized() - { - if (valueProperties == null) - { - valueProperties = new Dictionary(FieldData.Count); - - foreach (var kvp in FieldData) - { - valueProperties.Add(kvp.Key, new ContentFieldProperty(this, kvp.Value)); - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs deleted file mode 100644 index ed5aa34d2..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Jint.Native; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper -{ - public sealed class ContentFieldProperty : CustomProperty - { - private readonly ContentFieldObject contentField; - private IJsonValue contentValue; - private JsValue value; - private bool isChanged; - - protected override JsValue CustomValue - { - get - { - return value ?? (value = JsonMapper.Map(contentValue, contentField.Engine)); - } - set - { - if (!Equals(this.value, value)) - { - this.value = value; - - contentValue = null; - contentField.MarkChanged(); - - isChanged = true; - } - } - } - - public IJsonValue ContentValue - { - get { return contentValue ?? (contentValue = JsonMapper.Map(value)); } - } - - public bool IsChanged - { - get { return isChanged; } - } - - public ContentFieldProperty(ContentFieldObject contentField, IJsonValue contentValue = null) - { - this.contentField = contentField; - this.contentValue = contentValue; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs deleted file mode 100644 index 4720ddfce..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs +++ /dev/null @@ -1,131 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Jint; -using Jint.Native; -using Jint.Native.Object; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper -{ - public static class JsonMapper - { - public static JsValue Map(IJsonValue value, Engine engine) - { - if (value == null) - { - return JsValue.Null; - } - - switch (value) - { - case JsonNull _: - return JsValue.Null; - case JsonScalar s: - return new JsString(s.Value); - case JsonScalar b: - return new JsBoolean(b.Value); - case JsonScalar b: - return new JsNumber(b.Value); - case JsonObject obj: - return FromObject(obj, engine); - case JsonArray arr: - return FromArray(arr, engine); - } - - throw new ArgumentException("Invalid json type.", nameof(value)); - } - - private static JsValue FromArray(JsonArray arr, Engine engine) - { - var target = new JsValue[arr.Count]; - - for (var i = 0; i < arr.Count; i++) - { - target[i] = Map(arr[i], engine); - } - - return engine.Array.Construct(target); - } - - private static JsValue FromObject(JsonObject obj, Engine engine) - { - var target = new ObjectInstance(engine); - - foreach (var property in obj) - { - target.FastAddProperty(property.Key, Map(property.Value, engine), false, true, true); - } - - return target; - } - - public static IJsonValue Map(JsValue value) - { - if (value == null || value.IsNull() || value.IsUndefined()) - { - return JsonValue.Null; - } - - if (value.IsString()) - { - return JsonValue.Create(value.AsString()); - } - - if (value.IsBoolean()) - { - return JsonValue.Create(value.AsBoolean()); - } - - if (value.IsNumber()) - { - return JsonValue.Create(value.AsNumber()); - } - - if (value.IsDate()) - { - return JsonValue.Create(value.AsDate().ToString()); - } - - if (value.IsRegExp()) - { - return JsonValue.Create(value.AsRegExp().Value?.ToString()); - } - - if (value.IsArray()) - { - var arr = value.AsArray(); - - var result = JsonValue.Array(); - - for (var i = 0; i < arr.GetLength(); i++) - { - result.Add(Map(arr.Get(i.ToString()))); - } - - return result; - } - - if (value.IsObject()) - { - var obj = value.AsObject(); - - var result = JsonValue.Object(); - - foreach (var kvp in obj.GetOwnProperties()) - { - result[kvp.Key] = Map(kvp.Value.Value); - } - - return result; - } - - throw new ArgumentException("Invalid json type.", nameof(value)); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs deleted file mode 100644 index 4cd2a9007..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Security.Claims; -using Jint; -using Jint.Native; -using Jint.Runtime.Interop; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Core.Scripting -{ - public sealed class DefaultConverter : IObjectConverter - { - public static readonly DefaultConverter Instance = new DefaultConverter(); - - private DefaultConverter() - { - } - - public bool TryConvert(Engine engine, object value, out JsValue result) - { - result = null; - - if (value is Enum) - { - result = value.ToString(); - return true; - } - - switch (value) - { - case IUser user: - result = JintUser.Create(engine, user); - return true; - case ClaimsPrincipal principal: - result = JintUser.Create(engine, principal); - return true; - case Instant instant: - result = JsValue.FromObject(engine, instant.ToDateTimeUtc()); - return true; - case Status status: - result = status.ToString(); - return true; - case NamedContentData content: - result = new ContentDataObject(engine, content); - return true; - } - - return false; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs deleted file mode 100644 index 55742c99d..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Contents; - -namespace Squidex.Domain.Apps.Core.Scripting -{ - public interface IScriptEngine - { - void Execute(ScriptContext context, string script); - - NamedContentData ExecuteAndTransform(ScriptContext context, string script); - - NamedContentData Transform(ScriptContext context, string script); - - bool Evaluate(string name, object context, string script); - - string Interpolate(string name, object context, string script, Dictionary> customFormatters = null); - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs deleted file mode 100644 index 4f04a6343..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ /dev/null @@ -1,312 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Globalization; -using Esprima; -using Jint; -using Jint.Native; -using Jint.Native.Date; -using Jint.Native.Object; -using Jint.Runtime; -using Jint.Runtime.Interop; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Core.Scripting -{ - public sealed class JintScriptEngine : IScriptEngine - { - public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200); - - public void Execute(ScriptContext context, string script) - { - Guard.NotNull(context, nameof(context)); - - if (!string.IsNullOrWhiteSpace(script)) - { - var engine = CreateScriptEngine(context); - - EnableDisallow(engine); - EnableReject(engine); - - Execute(engine, script); - } - } - - public NamedContentData ExecuteAndTransform(ScriptContext context, string script) - { - Guard.NotNull(context, nameof(context)); - - var result = context.Data; - - if (!string.IsNullOrWhiteSpace(script)) - { - var engine = CreateScriptEngine(context); - - EnableDisallow(engine); - EnableReject(engine); - - engine.SetValue("operation", new Action(() => - { - var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); - - if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) - { - data.TryUpdate(out result); - } - })); - - engine.SetValue("replace", new Action(() => - { - var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); - - if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) - { - data.TryUpdate(out result); - } - })); - - Execute(engine, script); - } - - return result; - } - - public NamedContentData Transform(ScriptContext context, string script) - { - Guard.NotNull(context, nameof(context)); - - var result = context.Data; - - if (!string.IsNullOrWhiteSpace(script)) - { - try - { - var engine = CreateScriptEngine(context); - - engine.SetValue("replace", new Action(() => - { - var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); - - if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) - { - data.TryUpdate(out result); - } - })); - - engine.Execute(script); - } - catch (Exception) - { - result = context.Data; - } - } - - return result; - } - - private static void Execute(Engine engine, string script) - { - try - { - engine.Execute(script); - } - catch (ArgumentException ex) - { - throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message)); - } - catch (JavaScriptException ex) - { - throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message)); - } - catch (ParserException ex) - { - throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message)); - } - } - - private Engine CreateScriptEngine(ScriptContext context) - { - var engine = CreateScriptEngine(); - - var contextInstance = new ObjectInstance(engine); - - if (context.Data != null) - { - contextInstance.FastAddProperty("data", new ContentDataObject(engine, context.Data), true, true, true); - } - - if (context.DataOld != null) - { - contextInstance.FastAddProperty("oldData", new ContentDataObject(engine, context.DataOld), true, true, true); - } - - if (context.User != null) - { - contextInstance.FastAddProperty("user", JintUser.Create(engine, context.User), false, true, false); - } - - if (!string.IsNullOrWhiteSpace(context.Operation)) - { - contextInstance.FastAddProperty("operation", context.Operation, false, false, false); - } - - contextInstance.FastAddProperty("status", context.Status.ToString(), false, false, false); - - if (context.StatusOld != default) - { - contextInstance.FastAddProperty("oldStatus", context.StatusOld.ToString(), false, false, false); - } - - engine.SetValue("ctx", contextInstance); - engine.SetValue("context", contextInstance); - - return engine; - } - - private Engine CreateScriptEngine(IReferenceResolver resolver = null, Dictionary> customFormatters = null) - { - var engine = new Engine(options => - { - if (resolver != null) - { - options.SetReferencesResolver(resolver); - } - - options.TimeoutInterval(Timeout).Strict().AddObjectConverter(DefaultConverter.Instance); - }); - - if (customFormatters != null) - { - foreach (var kvp in customFormatters) - { - engine.SetValue(kvp.Key, Safe(kvp.Value)); - } - } - - engine.SetValue("slugify", new ClrFunctionInstance(engine, "slugify", Slugify)); - engine.SetValue("formatTime", new ClrFunctionInstance(engine, "formatTime", FormatDate)); - engine.SetValue("formatDate", new ClrFunctionInstance(engine, "formatDate", FormatDate)); - - return engine; - } - - private static Func Safe(Func func) - { - return () => - { - try - { - return func(); - } - catch - { - return "null"; - } - }; - } - - private static JsValue Slugify(JsValue thisObject, JsValue[] arguments) - { - try - { - var stringInput = TypeConverter.ToString(arguments.At(0)); - var single = false; - - if (arguments.Length > 1) - { - single = TypeConverter.ToBoolean(arguments.At(1)); - } - - return stringInput.Slugify(null, single); - } - catch - { - return JsValue.Undefined; - } - } - - private static JsValue FormatDate(JsValue thisObject, JsValue[] arguments) - { - try - { - var dateValue = ((DateInstance)arguments.At(0)).ToDateTime(); - var dateFormat = TypeConverter.ToString(arguments.At(1)); - - return dateValue.ToString(dateFormat, CultureInfo.InvariantCulture); - } - catch - { - return JsValue.Undefined; - } - } - - private static void EnableDisallow(Engine engine) - { - engine.SetValue("disallow", new Action(message => - { - var exMessage = !string.IsNullOrWhiteSpace(message) ? message : "Not allowed"; - - throw new DomainForbiddenException(exMessage); - })); - } - - private static void EnableReject(Engine engine) - { - engine.SetValue("reject", new Action(message => - { - var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null; - - throw new ValidationException("Script rejected the operation.", errors); - })); - } - - public bool Evaluate(string name, object context, string script) - { - try - { - var result = - CreateScriptEngine(NullPropagation.Instance) - .SetValue(name, context) - .Execute(script) - .GetCompletionValue() - .ToObject(); - - return (bool)result; - } - catch - { - return false; - } - } - - public string Interpolate(string name, object context, string script, Dictionary> customFormatters = null) - { - try - { - var result = - CreateScriptEngine(NullPropagation.Instance, customFormatters) - .SetValue(name, context) - .Execute(script) - .GetCompletionValue() - .ToObject(); - - var converted = result.ToString(); - - return converted == "undefined" ? "null" : converted; - } - catch (Exception ex) - { - return ex.Message; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs deleted file mode 100644 index d32a234bd..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using Jint; -using Jint.Runtime.Interop; -using Squidex.Infrastructure.Security; -using Squidex.Shared.Identity; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Core.Scripting -{ - public static class JintUser - { - private static readonly char[] ClaimSeparators = { '/', '.', ':' }; - - public static ObjectWrapper Create(Engine engine, IUser user) - { - var clientId = user.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value; - - var isClient = !string.IsNullOrWhiteSpace(clientId); - - return CreateUser(engine, user.Id, isClient, user.Email, user.DisplayName(), user.Claims); - } - - public static ObjectWrapper Create(Engine engine, ClaimsPrincipal principal) - { - var id = principal.OpenIdSubject(); - - var isClient = string.IsNullOrWhiteSpace(id); - - if (isClient) - { - id = principal.OpenIdClientId(); - } - - var name = principal.FindFirst(SquidexClaimTypes.DisplayName)?.Value; - - return CreateUser(engine, id, isClient, principal.OpenIdEmail(), name, principal.Claims); - } - - private static ObjectWrapper CreateUser(Engine engine, string id, bool isClient, string email, string name, IEnumerable allClaims) - { - var claims = - allClaims.GroupBy(x => x.Type.Split(ClaimSeparators).Last()) - .ToDictionary( - x => x.Key, - x => x.Select(y => y.Value).ToArray()); - - return new ObjectWrapper(engine, new { id, isClient, email, name, claims }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs deleted file mode 100644 index 40d4212ae..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Security.Claims; -using Squidex.Domain.Apps.Core.Contents; - -namespace Squidex.Domain.Apps.Core.Scripting -{ - public sealed class ScriptContext - { - public ClaimsPrincipal User { get; set; } - - public Guid ContentId { get; set; } - - public NamedContentData Data { get; set; } - - public NamedContentData DataOld { get; set; } - - public Status Status { get; set; } - - public Status StatusOld { get; set; } - - public string Operation { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj deleted file mode 100644 index 07d2f40f4..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - netstandard2.0 - Squidex.Domain.Apps.Core - 7.3 - - - full - True - - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs deleted file mode 100644 index ad819ba57..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Core.Tags -{ - public interface ITagService - { - Task> GetTagIdsAsync(Guid appId, string group, HashSet names); - - Task> NormalizeTagsAsync(Guid appId, string group, HashSet names, HashSet ids); - - Task> DenormalizeTagsAsync(Guid appId, string group, HashSet ids); - - Task GetTagsAsync(Guid appId, string group); - - Task GetExportableTagsAsync(Guid appId, string group); - - Task RebuildTagsAsync(Guid appId, string group, TagsExport tags); - - Task ClearAsync(Guid appId, string group); - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs deleted file mode 100644 index 7ecac0baf..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs +++ /dev/null @@ -1,150 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.Tags -{ - public static class TagNormalizer - { - public static async Task NormalizeAsync(this ITagService tagService, Guid appId, Guid schemaId, Schema schema, NamedContentData newData, NamedContentData oldData) - { - Guard.NotNull(tagService, nameof(tagService)); - Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(newData, nameof(newData)); - - var newValues = new HashSet(); - var newArrays = new List(); - - var oldValues = new HashSet(); - var oldArrays = new List(); - - GetValues(schema, newValues, newArrays, newData); - - if (oldData != null) - { - GetValues(schema, oldValues, oldArrays, oldData); - } - - if (newValues.Count > 0) - { - var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), newValues, oldValues); - - foreach (var array in newArrays) - { - for (var i = 0; i < array.Count; i++) - { - if (normalized.TryGetValue(array[i].ToString(), out var result)) - { - array[i] = JsonValue.Create(result); - } - } - } - } - } - - public static async Task DenormalizeAsync(this ITagService tagService, Guid appId, Guid schemaId, Schema schema, params NamedContentData[] datas) - { - Guard.NotNull(tagService, nameof(tagService)); - Guard.NotNull(schema, nameof(schema)); - - var tagsValues = new HashSet(); - var tagsArrays = new List(); - - GetValues(schema, tagsValues, tagsArrays, datas); - - if (tagsValues.Count > 0) - { - var denormalized = await tagService.DenormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), tagsValues); - - foreach (var array in tagsArrays) - { - for (var i = 0; i < array.Count; i++) - { - if (denormalized.TryGetValue(array[i].ToString(), out var result)) - { - array[i] = JsonValue.Create(result); - } - } - } - } - } - - private static void GetValues(Schema schema, HashSet values, List arrays, params NamedContentData[] datas) - { - foreach (var field in schema.Fields) - { - if (field is IField tags && tags.Properties.Normalization == TagsFieldNormalization.Schema) - { - foreach (var data in datas) - { - if (data.TryGetValue(field.Name, out var fieldData)) - { - foreach (var partition in fieldData) - { - ExtractTags(partition.Value, values, arrays); - } - } - } - } - else if (field is IArrayField arrayField) - { - foreach (var nestedField in arrayField.Fields) - { - if (nestedField is IField nestedTags && nestedTags.Properties.Normalization == TagsFieldNormalization.Schema) - { - foreach (var data in datas) - { - if (data.TryGetValue(field.Name, out var fieldData)) - { - foreach (var partition in fieldData) - { - if (partition.Value is JsonArray array) - { - foreach (var value in array) - { - if (value is JsonObject nestedObject) - { - if (nestedObject.TryGetValue(nestedField.Name, out var nestedValue)) - { - ExtractTags(nestedValue, values, arrays); - } - } - } - } - } - } - } - } - } - } - } - } - - private static void ExtractTags(IJsonValue value, ISet values, ICollection arrays) - { - if (value is JsonArray array) - { - foreach (var item in array) - { - if (item.Type == JsonValueType.String) - { - values.Add(item.ToString()); - } - } - - arrays.Add(array); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs deleted file mode 100644 index ca3c908a6..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs +++ /dev/null @@ -1,108 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent.Validators; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Validation; - -#pragma warning disable SA1028, IDE0004 // Code must not contain trailing whitespace - -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public sealed class ContentValidator - { - private readonly Schema schema; - private readonly PartitionResolver partitionResolver; - private readonly ValidationContext context; - private readonly ConcurrentBag errors = new ConcurrentBag(); - - public IReadOnlyCollection Errors - { - get { return errors; } - } - - public ContentValidator(Schema schema, PartitionResolver partitionResolver, ValidationContext context) - { - Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(context, nameof(context)); - Guard.NotNull(partitionResolver, nameof(partitionResolver)); - - this.schema = schema; - this.context = context; - this.partitionResolver = partitionResolver; - } - - private void AddError(IEnumerable path, string message) - { - var pathString = path.ToPathString(); - - errors.Add(new ValidationError(message, pathString)); - } - - public Task ValidatePartialAsync(NamedContentData data) - { - Guard.NotNull(data, nameof(data)); - - var validator = CreateSchemaValidator(true); - - return validator.ValidateAsync(data, context, AddError); - } - - public Task ValidateAsync(NamedContentData data) - { - Guard.NotNull(data, nameof(data)); - - var validator = CreateSchemaValidator(false); - - return validator.ValidateAsync(data, context, AddError); - } - - private IValidator CreateSchemaValidator(bool isPartial) - { - var fieldsValidators = new Dictionary(schema.Fields.Count); - - foreach (var field in schema.Fields) - { - fieldsValidators[field.Name] = (!field.RawProperties.IsRequired, CreateFieldValidator(field, isPartial)); - } - - return new ObjectValidator(fieldsValidators, isPartial, "field"); - } - - private IValidator CreateFieldValidator(IRootField field, bool isPartial) - { - var partitioning = partitionResolver(field.Partitioning); - - var fieldValidator = field.CreateValidator(); - var fieldsValidators = new Dictionary(); - - foreach (var partition in partitioning) - { - fieldsValidators[partition.Key] = (partition.IsOptional, fieldValidator); - } - - return new AggregateValidator( - field.CreateBagValidator() - .Union(Enumerable.Repeat( - new ObjectValidator(fieldsValidators, isPartial, TypeName(field)), 1))); - } - - private static string TypeName(IRootField field) - { - var isLanguage = field.Partitioning.Equals(Partitioning.Language); - - return isLanguage ? "language" : "invariant value"; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs deleted file mode 100644 index 79747c6b8..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent.Validators; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public sealed class FieldBagValidatorsFactory : IFieldVisitor> - { - private static readonly FieldBagValidatorsFactory Instance = new FieldBagValidatorsFactory(); - - private FieldBagValidatorsFactory() - { - } - - public static IEnumerable CreateValidators(IField field) - { - Guard.NotNull(field, nameof(field)); - - return field.Accept(Instance); - } - - public IEnumerable Visit(IArrayField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield return NoValueValidator.Instance; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs deleted file mode 100644 index 95e179558..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs +++ /dev/null @@ -1,191 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NodaTime; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent.Validators; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public sealed class FieldValueValidatorsFactory : IFieldVisitor> - { - private static readonly FieldValueValidatorsFactory Instance = new FieldValueValidatorsFactory(); - - private FieldValueValidatorsFactory() - { - } - - public static IEnumerable CreateValidators(IField field) - { - Guard.NotNull(field, nameof(field)); - - return field.Accept(Instance); - } - - public IEnumerable Visit(IArrayField field) - { - if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) - { - yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); - } - - var nestedSchema = new Dictionary(field.Fields.Count); - - foreach (var nestedField in field.Fields) - { - nestedSchema[nestedField.Name] = (false, nestedField.CreateValidator()); - } - - yield return new CollectionItemValidator(new ObjectValidator(nestedSchema, false, "field")); - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) - { - yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); - } - - if (!field.Properties.AllowDuplicates) - { - yield return new UniqueValuesValidator(); - } - - yield return new AssetsValidator(field.Properties); - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired) - { - yield return new RequiredValidator(); - } - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired) - { - yield return new RequiredValidator(); - } - - if (field.Properties.MinValue.HasValue || field.Properties.MaxValue.HasValue) - { - yield return new RangeValidator(field.Properties.MinValue, field.Properties.MaxValue); - } - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired) - { - yield return new RequiredValidator(); - } - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired) - { - yield return new RequiredValidator(); - } - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired) - { - yield return new RequiredValidator(); - } - - if (field.Properties.MinValue.HasValue || field.Properties.MaxValue.HasValue) - { - yield return new RangeValidator(field.Properties.MinValue, field.Properties.MaxValue); - } - - if (field.Properties.AllowedValues != null) - { - yield return new AllowedValuesValidator(field.Properties.AllowedValues); - } - - if (field.Properties.IsUnique) - { - yield return new UniqueValidator(); - } - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) - { - yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); - } - - if (!field.Properties.AllowDuplicates) - { - yield return new UniqueValuesValidator(); - } - - yield return new ReferencesValidator(field.Properties.SchemaIds); - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired) - { - yield return new RequiredStringValidator(true); - } - - if (field.Properties.MinLength.HasValue || field.Properties.MaxLength.HasValue) - { - yield return new StringLengthValidator(field.Properties.MinLength, field.Properties.MaxLength); - } - - if (!string.IsNullOrWhiteSpace(field.Properties.Pattern)) - { - yield return new PatternValidator(field.Properties.Pattern, field.Properties.PatternMessage); - } - - if (field.Properties.AllowedValues != null) - { - yield return new AllowedValuesValidator(field.Properties.AllowedValues); - } - - if (field.Properties.IsUnique) - { - yield return new UniqueValidator(); - } - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) - { - yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); - } - - if (field.Properties.AllowedValues != null) - { - yield return new CollectionItemValidator(new AllowedValuesValidator(field.Properties.AllowedValues)); - } - - yield return new CollectionItemValidator(new RequiredStringValidator(true)); - } - - public IEnumerable Visit(IField field) - { - if (field is INestedField) - { - yield return NoValueValidator.Instance; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs deleted file mode 100644 index 484cb8e38..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs +++ /dev/null @@ -1,231 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NodaTime.Text; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public sealed class JsonValueConverter : IFieldVisitor - { - private readonly IJsonValue value; - - private JsonValueConverter(IJsonValue value) - { - this.value = value; - } - - public static object ConvertValue(IField field, IJsonValue json) - { - return field.Accept(new JsonValueConverter(json)); - } - - public object Visit(IArrayField field) - { - return ConvertToObjectList(); - } - - public object Visit(IField field) - { - return ConvertToGuidList(); - } - - public object Visit(IField field) - { - return ConvertToGuidList(); - } - - public object Visit(IField field) - { - return ConvertToStringList(); - } - - public object Visit(IField field) - { - if (value is JsonScalar b) - { - return b.Value; - } - - throw new InvalidCastException("Invalid json type, expected boolean."); - } - - public object Visit(IField field) - { - if (value is JsonScalar b) - { - return b.Value; - } - - throw new InvalidCastException("Invalid json type, expected number."); - } - - public object Visit(IField field) - { - if (value is JsonScalar b) - { - return b.Value; - } - - throw new InvalidCastException("Invalid json type, expected string."); - } - - public object Visit(IField field) - { - return value; - } - - public object Visit(IField field) - { - if (value.Type == JsonValueType.String) - { - var parseResult = InstantPattern.General.Parse(value.ToString()); - - if (!parseResult.Success) - { - throw parseResult.Exception; - } - - return parseResult.Value; - } - - throw new InvalidCastException("Invalid json type, expected string."); - } - - public object Visit(IField field) - { - if (value is JsonObject geolocation) - { - foreach (var propertyName in geolocation.Keys) - { - if (!string.Equals(propertyName, "latitude", StringComparison.OrdinalIgnoreCase) && - !string.Equals(propertyName, "longitude", StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidCastException("Geolocation can only have latitude and longitude property."); - } - } - - if (geolocation.TryGetValue("latitude", out var latValue) && latValue is JsonScalar latNumber) - { - var lat = latNumber.Value; - - if (!lat.IsBetween(-90, 90)) - { - throw new InvalidCastException("Latitude must be between -90 and 90."); - } - } - else - { - throw new InvalidCastException("Invalid json type, expected latitude/longitude object."); - } - - if (geolocation.TryGetValue("longitude", out var lonValue) && lonValue is JsonScalar lonNumber) - { - var lon = lonNumber.Value; - - if (!lon.IsBetween(-180, 180)) - { - throw new InvalidCastException("Longitude must be between -180 and 180."); - } - } - else - { - throw new InvalidCastException("Invalid json type, expected latitude/longitude object."); - } - - return value; - } - - throw new InvalidCastException("Invalid json type, expected latitude/longitude object."); - } - - public object Visit(IField field) - { - return value; - } - - private object ConvertToGuidList() - { - if (value is JsonArray array) - { - var result = new List(); - - foreach (var item in array) - { - if (item is JsonScalar s && Guid.TryParse(s.Value, out var guid)) - { - result.Add(guid); - } - else - { - throw new InvalidCastException("Invalid json type, expected array of guid strings."); - } - } - - return result; - } - - throw new InvalidCastException("Invalid json type, expected array of guid strings."); - } - - private object ConvertToStringList() - { - if (value is JsonArray array) - { - var result = new List(); - - foreach (var item in array) - { - if (item is JsonNull) - { - result.Add(null); - } - else if (item is JsonScalar s) - { - result.Add(s.Value); - } - else - { - throw new InvalidCastException("Invalid json type, expected array of strings."); - } - } - - return result; - } - - throw new InvalidCastException("Invalid json type, expected array of strings."); - } - - private object ConvertToObjectList() - { - if (value is JsonArray array) - { - var result = new List(); - - foreach (var item in array) - { - if (item is JsonObject obj) - { - result.Add(obj); - } - else - { - throw new InvalidCastException("Invalid json type, expected array of objects."); - } - } - - return result; - } - - throw new InvalidCastException("Invalid json type, expected array of objects."); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs deleted file mode 100644 index a15507007..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public static class Undefined - { - public static readonly object Value = new object(); - - public static bool IsUndefined(this object other) - { - return ReferenceEquals(other, Value); - } - - public static bool IsNullOrUndefined(this object other) - { - return other == null || other.IsUndefined(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs deleted file mode 100644 index ec4740c39..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs +++ /dev/null @@ -1,127 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public delegate Task> CheckContents(Guid schemaId, FilterNode filter); - - public delegate Task> CheckContentsByIds(HashSet ids); - - public delegate Task> CheckAssets(IEnumerable ids); - - public sealed class ValidationContext - { - private readonly Guid contentId; - private readonly Guid schemaId; - private readonly CheckContents checkContent; - private readonly CheckContentsByIds checkContentByIds; - private readonly CheckAssets checkAsset; - private readonly ImmutableQueue propertyPath; - - public ImmutableQueue Path - { - get { return propertyPath; } - } - - public Guid ContentId - { - get { return contentId; } - } - - public Guid SchemaId - { - get { return schemaId; } - } - - public bool IsOptional { get; } - - public ValidationContext( - Guid contentId, - Guid schemaId, - CheckContents checkContent, - CheckContentsByIds checkContentsByIds, - CheckAssets checkAsset) - : this(contentId, schemaId, checkContent, checkContentsByIds, checkAsset, ImmutableQueue.Empty, false) - { - } - - private ValidationContext( - Guid contentId, - Guid schemaId, - CheckContents checkContent, - CheckContentsByIds checkContentByIds, - CheckAssets checkAsset, - ImmutableQueue propertyPath, - bool isOptional) - { - Guard.NotNull(checkAsset, nameof(checkAsset)); - Guard.NotNull(checkContent, nameof(checkContent)); - Guard.NotNull(checkContentByIds, nameof(checkContentByIds)); - - this.propertyPath = propertyPath; - - this.checkContent = checkContent; - this.checkContentByIds = checkContentByIds; - this.checkAsset = checkAsset; - this.contentId = contentId; - - this.schemaId = schemaId; - - IsOptional = isOptional; - } - - public ValidationContext Optional(bool isOptional) - { - return isOptional == IsOptional ? this : OptionalCore(isOptional); - } - - private ValidationContext OptionalCore(bool isOptional) - { - return new ValidationContext( - contentId, - schemaId, - checkContent, - checkContentByIds, - checkAsset, - propertyPath, - isOptional); - } - - public ValidationContext Nested(string property) - { - return new ValidationContext( - contentId, schemaId, - checkContent, - checkContentByIds, - checkAsset, - propertyPath.Enqueue(property), - IsOptional); - } - - public Task> GetContentIdsAsync(HashSet ids) - { - return checkContentByIds(ids); - } - - public Task> GetContentIdsAsync(Guid schemaId, FilterNode filter) - { - return checkContent(schemaId, filter); - } - - public Task> GetAssetInfosAsync(IEnumerable assetId) - { - return checkAsset(assetId); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs deleted file mode 100644 index 16b842801..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class AggregateValidator : IValidator - { - private readonly IValidator[] validators; - - public AggregateValidator(IEnumerable validators) - { - this.validators = validators?.ToArray(); - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (validators?.Length > 0) - { - return Task.WhenAll(validators.Select(x => x.ValidateAsync(value, context, addError))); - } - - return Task.CompletedTask; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs deleted file mode 100644 index 6f0d4a63a..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class AllowedValuesValidator : IValidator - { - private readonly IEnumerable allowedValues; - - public AllowedValuesValidator(params T[] allowedValues) - : this((IEnumerable)allowedValues) - { - } - - public AllowedValuesValidator(IEnumerable allowedValues) - { - Guard.NotNull(allowedValues, nameof(allowedValues)); - - this.allowedValues = allowedValues; - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value != null && value is T typedValue && !allowedValues.Contains(typedValue)) - { - addError(context.Path, "Not an allowed value."); - } - - return TaskHelper.Done; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs deleted file mode 100644 index f1a87c283..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs +++ /dev/null @@ -1,116 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class AssetsValidator : IValidator - { - private readonly AssetsFieldProperties properties; - - public AssetsValidator(AssetsFieldProperties properties) - { - this.properties = properties; - } - - public async Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value is ICollection assetIds && assetIds.Count > 0) - { - var assets = await context.GetAssetInfosAsync(assetIds); - var index = 0; - - foreach (var assetId in assetIds) - { - index++; - - var path = context.Path.Enqueue($"[{index}]"); - - var asset = assets.FirstOrDefault(x => x.AssetId == assetId); - - if (asset == null) - { - addError(path, $"Id '{assetId}' not found."); - continue; - } - - if (properties.MinSize.HasValue && asset.FileSize < properties.MinSize) - { - addError(path, $"'{asset.FileSize.ToReadableSize()}' less than minimum of '{properties.MinSize.Value.ToReadableSize()}'."); - } - - if (properties.MaxSize.HasValue && asset.FileSize > properties.MaxSize) - { - addError(path, $"'{asset.FileSize.ToReadableSize()}' greater than maximum of '{properties.MaxSize.Value.ToReadableSize()}'."); - } - - if (properties.AllowedExtensions != null && - properties.AllowedExtensions.Count > 0 && - !properties.AllowedExtensions.Any(x => asset.FileName.EndsWith("." + x, StringComparison.OrdinalIgnoreCase))) - { - addError(path, "Invalid file extension."); - } - - if (!asset.IsImage) - { - if (properties.MustBeImage) - { - addError(path, "Not an image."); - } - - continue; - } - - if (asset.PixelWidth.HasValue && - asset.PixelHeight.HasValue) - { - var w = asset.PixelWidth.Value; - var h = asset.PixelHeight.Value; - - var actualRatio = (double)w / h; - - if (properties.MinWidth.HasValue && w < properties.MinWidth) - { - addError(path, $"Width '{w}px' less than minimum of '{properties.MinWidth}px'."); - } - - if (properties.MaxWidth.HasValue && w > properties.MaxWidth) - { - addError(path, $"Width '{w}px' greater than maximum of '{properties.MaxWidth}px'."); - } - - if (properties.MinHeight.HasValue && h < properties.MinHeight) - { - addError(path, $"Height '{h}px' less than minimum of '{properties.MinHeight}px'."); - } - - if (properties.MaxHeight.HasValue && h > properties.MaxHeight) - { - addError(path, $"Height '{h}px' greater than maximum of '{properties.MaxHeight}px'."); - } - - if (properties.AspectHeight.HasValue && properties.AspectWidth.HasValue) - { - var expectedRatio = (double)properties.AspectWidth.Value / properties.AspectHeight.Value; - - if (Math.Abs(expectedRatio - actualRatio) > double.Epsilon) - { - addError(path, $"Aspect ratio not '{properties.AspectWidth}:{properties.AspectHeight}'."); - } - } - } - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs deleted file mode 100644 index 8e8efdd46..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class CollectionItemValidator : IValidator - { - private readonly IValidator[] itemValidators; - - public CollectionItemValidator(params IValidator[] itemValidators) - { - Guard.NotNull(itemValidators, nameof(itemValidators)); - Guard.NotEmpty(itemValidators, nameof(itemValidators)); - - this.itemValidators = itemValidators; - } - - public async Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value is ICollection items && items.Count > 0) - { - var innerTasks = new List(); - var index = 1; - - foreach (var item in items) - { - var innerContext = context.Nested($"[{index}]"); - - foreach (var itemValidator in itemValidators) - { - innerTasks.Add(itemValidator.ValidateAsync(item, innerContext, addError)); - } - - index++; - } - - await Task.WhenAll(innerTasks); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs deleted file mode 100644 index a858b811c..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class CollectionValidator : IValidator - { - private readonly bool isRequired; - private readonly int? minItems; - private readonly int? maxItems; - - public CollectionValidator(bool isRequired, int? minItems = null, int? maxItems = null) - { - if (minItems.HasValue && maxItems.HasValue && minItems.Value > maxItems.Value) - { - throw new ArgumentException("Min length must be greater than max length.", nameof(minItems)); - } - - this.isRequired = isRequired; - this.minItems = minItems; - this.maxItems = maxItems; - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (!(value is ICollection items) || items.Count == 0) - { - if (isRequired && !context.IsOptional) - { - addError(context.Path, "Field is required."); - } - - return TaskHelper.Done; - } - - if (minItems.HasValue && maxItems.HasValue) - { - if (minItems == maxItems && minItems != items.Count) - { - addError(context.Path, $"Must have exactly {maxItems} item(s)."); - } - else if (items.Count < minItems || items.Count > maxItems) - { - addError(context.Path, $"Must have between {minItems} and {maxItems} item(s)."); - } - } - else - { - if (minItems.HasValue && items.Count < minItems.Value) - { - addError(context.Path, $"Must have at least {minItems} item(s)."); - } - - if (maxItems.HasValue && items.Count > maxItems.Value) - { - addError(context.Path, $"Must not have more than {maxItems} item(s)."); - } - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs deleted file mode 100644 index 8f2f2689c..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class FieldValidator : IValidator - { - private readonly IValidator[] validators; - private readonly IField field; - - public FieldValidator(IEnumerable validators, IField field) - { - Guard.NotNull(field, nameof(field)); - - this.validators = validators.ToArray(); - - this.field = field; - } - - public async Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - try - { - var typedValue = value; - - if (value is IJsonValue jsonValue) - { - if (jsonValue.Type == JsonValueType.Null) - { - typedValue = null; - } - else - { - typedValue = JsonValueConverter.ConvertValue(field, jsonValue); - } - } - - if (validators?.Length > 0) - { - var tasks = new List(); - - foreach (var validator in validators) - { - tasks.Add(validator.ValidateAsync(typedValue, context, addError)); - } - - await Task.WhenAll(tasks); - } - } - catch - { - addError(context.Path, "Not a valid value."); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs deleted file mode 100644 index 47592700f..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public delegate void AddError(IEnumerable path, string message); - - public interface IValidator - { - Task ValidateAsync(object value, ValidationContext context, AddError addError); - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs deleted file mode 100644 index 835a10d31..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class NoValueValidator : IValidator - { - public static readonly NoValueValidator Instance = new NoValueValidator(); - - private NoValueValidator() - { - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (!value.IsUndefined()) - { - addError(context.Path, "Value must not be defined."); - } - - return Task.CompletedTask; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs deleted file mode 100644 index c86c85be3..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class ObjectValidator : IValidator - { - private static readonly IReadOnlyDictionary DefaultValue = new Dictionary(); - private readonly IDictionary schema; - private readonly bool isPartial; - private readonly string fieldType; - - public ObjectValidator(IDictionary schema, bool isPartial, string fieldType) - { - this.schema = schema; - this.fieldType = fieldType; - this.isPartial = isPartial; - } - - public async Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value.IsNullOrUndefined()) - { - value = DefaultValue; - } - - if (value is IReadOnlyDictionary values) - { - foreach (var fieldData in values) - { - var name = fieldData.Key; - - if (!schema.ContainsKey(name)) - { - addError(context.Path.Enqueue(name), $"Not a known {fieldType}."); - } - } - - var tasks = new List(); - - foreach (var field in schema) - { - var name = field.Key; - - var (isOptional, validator) = field.Value; - - var fieldValue = Undefined.Value; - - if (!values.TryGetValue(name, out var temp)) - { - if (isPartial) - { - continue; - } - } - else - { - fieldValue = temp; - } - - var fieldContext = context.Nested(name).Optional(isOptional); - - tasks.Add(validator.ValidateAsync(fieldValue, fieldContext, addError)); - } - - await Task.WhenAll(tasks); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs deleted file mode 100644 index af952f7fb..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public class PatternValidator : IValidator - { - private static readonly TimeSpan Timeout = TimeSpan.FromMilliseconds(20); - private readonly Regex regex; - private readonly string errorMessage; - - public PatternValidator(string pattern, string errorMessage = null) - { - this.errorMessage = errorMessage; - - regex = new Regex("^" + pattern + "$", RegexOptions.None, Timeout); - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value is string stringValue) - { - if (!string.IsNullOrEmpty(stringValue)) - { - try - { - if (!regex.IsMatch(stringValue)) - { - if (string.IsNullOrWhiteSpace(errorMessage)) - { - addError(context.Path, "Does not match to the pattern."); - } - else - { - addError(context.Path, errorMessage); - } - } - } - catch - { - addError(context.Path, "Regex is too slow."); - } - } - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs deleted file mode 100644 index 2209786f7..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class RangeValidator : IValidator where T : struct, IComparable - { - private readonly T? min; - private readonly T? max; - - public RangeValidator(T? min, T? max) - { - if (min.HasValue && max.HasValue && min.Value.CompareTo(max.Value) > 0) - { - throw new ArgumentException("Min value must be greater than max value.", nameof(min)); - } - - this.min = min; - this.max = max; - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value != null && value is T typedValue) - { - if (min.HasValue && max.HasValue) - { - if (Equals(min, max) && Equals(min.Value, max.Value)) - { - addError(context.Path, $"Must be exactly '{max}'."); - } - else if (typedValue.CompareTo(min.Value) < 0 || typedValue.CompareTo(max.Value) > 0) - { - addError(context.Path, $"Must be between '{min}' and '{max}'."); - } - } - else - { - if (min.HasValue && typedValue.CompareTo(min.Value) < 0) - { - addError(context.Path, $"Must be greater or equal to '{min}'."); - } - - if (max.HasValue && typedValue.CompareTo(max.Value) > 0) - { - addError(context.Path, $"Must be less or equal to '{max}'."); - } - } - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs deleted file mode 100644 index 62ad9a34c..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class ReferencesValidator : IValidator - { - private readonly IEnumerable schemaIds; - - public ReferencesValidator(IEnumerable schemaIds) - { - this.schemaIds = schemaIds; - } - - public async Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value is ICollection contentIds) - { - var foundIds = await context.GetContentIdsAsync(contentIds.ToHashSet()); - - foreach (var id in contentIds) - { - var (schemaId, _) = foundIds.FirstOrDefault(x => x.Id == id); - - if (schemaId == Guid.Empty) - { - addError(context.Path, $"Contains invalid reference '{id}'."); - } - else if (schemaIds?.Any() == true && !schemaIds.Contains(schemaId)) - { - addError(context.Path, $"Contains reference '{id}' to invalid schema."); - } - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs deleted file mode 100644 index 129f88dab..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public class RequiredStringValidator : IValidator - { - private readonly bool validateEmptyStrings; - - public RequiredStringValidator(bool validateEmptyStrings = false) - { - this.validateEmptyStrings = validateEmptyStrings; - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (context.IsOptional) - { - return TaskHelper.Done; - } - - if (value.IsNullOrUndefined() || IsEmptyString(value)) - { - addError(context.Path, "Field is required."); - } - - return TaskHelper.Done; - } - - private bool IsEmptyString(object value) - { - return value is string typed && validateEmptyStrings && string.IsNullOrWhiteSpace(typed); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs deleted file mode 100644 index 6a92a3671..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public class RequiredValidator : IValidator - { - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value.IsNullOrUndefined() && !context.IsOptional) - { - addError(context.Path, "Field is required."); - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs deleted file mode 100644 index b6579a7bc..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public class StringLengthValidator : IValidator - { - private readonly int? minLength; - private readonly int? maxLength; - - public StringLengthValidator(int? minLength, int? maxLength) - { - if (minLength.HasValue && maxLength.HasValue && minLength.Value > maxLength.Value) - { - throw new ArgumentException("Min length must be greater than max length.", nameof(minLength)); - } - - this.minLength = minLength; - this.maxLength = maxLength; - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) - { - if (minLength.HasValue && maxLength.HasValue) - { - if (minLength == maxLength && minLength != stringValue.Length) - { - addError(context.Path, $"Must have exactly {maxLength} character(s)."); - } - else if (stringValue.Length < minLength || stringValue.Length > maxLength) - { - addError(context.Path, $"Must have between {minLength} and {maxLength} character(s)."); - } - } - else - { - if (minLength.HasValue && stringValue.Length < minLength.Value) - { - addError(context.Path, $"Must have at least {minLength} character(s)."); - } - - if (maxLength.HasValue && stringValue.Length > maxLength.Value) - { - addError(context.Path, $"Must not have more than {maxLength} character(s)."); - } - } - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs deleted file mode 100644 index 6fad491b9..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class UniqueValidator : IValidator - { - public async Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - var count = context.Path.Count(); - - if (value != null && (count == 0 || (count == 2 && context.Path.Last() == InvariantPartitioning.Key))) - { - FilterNode filter = null; - - if (value is string s) - { - filter = ClrFilter.Eq(Path(context), s); - } - else if (value is double d) - { - filter = ClrFilter.Eq(Path(context), d); - } - - if (filter != null) - { - var found = await context.GetContentIdsAsync(context.SchemaId, filter); - - if (found.Any(x => x.Id != context.ContentId)) - { - addError(context.Path, "Another content with the same value exists."); - } - } - } - } - - private static List Path(ValidationContext context) - { - return Enumerable.Repeat("Data", 1).Union(context.Path).ToList(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs deleted file mode 100644 index 7c948165b..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class UniqueValuesValidator : IValidator - { - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value is IEnumerable items && items.Any()) - { - var itemsArray = items.ToArray(); - - if (itemsArray.Length != itemsArray.Distinct().Count()) - { - addError(context.Path, "Must not contain duplicate values."); - } - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs deleted file mode 100644 index 800db6550..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ /dev/null @@ -1,148 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets -{ - public sealed partial class MongoAssetRepository : MongoRepositoryBase, IAssetRepository - { - public MongoAssetRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "States_Assets"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.Tags) - .Descending(x => x.LastModified)), - new CreateIndexModel( - Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.Slug)) - }, ct); - } - - public async Task> QueryAsync(Guid appId, ClrQuery query) - { - using (Profiler.TraceMethod("QueryAsyncByQuery")) - { - try - { - query = query.AdjustToModel(); - - var filter = query.BuildFilter(appId); - - var contentCount = Collection.Find(filter).CountDocumentsAsync(); - var contentItems = - Collection.Find(filter) - .AssetTake(query) - .AssetSkip(query) - .AssetSort(query) - .ToListAsync(); - - await Task.WhenAll(contentItems, contentCount); - - return ResultList.Create(contentCount.Result, contentItems.Result); - } - catch (MongoQueryException ex) - { - if (ex.Message.Contains("17406")) - { - throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items."); - } - else - { - throw; - } - } - } - } - - public async Task> QueryAsync(Guid appId, HashSet ids) - { - using (Profiler.TraceMethod("QueryAsyncByIds")) - { - var find = Collection.Find(x => ids.Contains(x.Id)).SortByDescending(x => x.LastModified); - - var assetItems = await find.ToListAsync(); - - return ResultList.Create(assetItems.Count, assetItems.OfType()); - } - } - - public async Task FindAssetBySlugAsync(Guid appId, string slug) - { - using (Profiler.TraceMethod()) - { - var assetEntity = - await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.Slug == slug) - .FirstOrDefaultAsync(); - - return assetEntity; - } - } - - public async Task> QueryByHashAsync(Guid appId, string hash) - { - using (Profiler.TraceMethod()) - { - var assetEntities = - await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.FileHash == hash) - .ToListAsync(); - - return assetEntities.OfType().ToList(); - } - } - - public async Task FindAssetAsync(Guid id, bool allowDeleted = false) - { - using (Profiler.TraceMethod()) - { - var assetEntity = - await Collection.Find(x => x.Id == id) - .FirstOrDefaultAsync(); - - if (assetEntity?.IsDeleted == true && !allowDeleted) - { - return null; - } - - return assetEntity; - } - } - - public Task RemoveAsync(Guid key) - { - return Collection.DeleteOneAsync(x => x.Id == key); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs deleted file mode 100644 index 0b4828446..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Assets.State; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets -{ - public sealed partial class MongoAssetRepository : ISnapshotStore - { - async Task<(AssetState Value, long Version)> ISnapshotStore.ReadAsync(Guid key) - { - using (Profiler.TraceMethod()) - { - var existing = - await Collection.Find(x => x.Id == key) - .FirstOrDefaultAsync(); - - if (existing != null) - { - return (Map(existing), existing.Version); - } - - return (null, EtagVersion.NotFound); - } - } - - async Task ISnapshotStore.WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion) - { - using (Profiler.TraceMethod()) - { - var entity = SimpleMapper.Map(value, new MongoAssetEntity()); - - entity.Version = newVersion; - entity.IndexedAppId = value.AppId.Id; - - await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, entity, Upsert); - } - } - - async Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct) - { - using (Profiler.TraceMethod()) - { - await Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(x => callback(Map(x), x.Version), ct); - } - } - - async Task ISnapshotStore.RemoveAsync(Guid key) - { - using (Profiler.TraceMethod()) - { - await Collection.DeleteOneAsync(x => x.Id == key); - } - } - - private static AssetState Map(MongoAssetEntity existing) - { - return SimpleMapper.Map(existing, new AssetState()); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs deleted file mode 100644 index 7988b72a7..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ /dev/null @@ -1,271 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.State; -using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents -{ - internal class MongoContentCollection : MongoRepositoryBase - { - private readonly IAppProvider appProvider; - private readonly IJsonSerializer serializer; - - public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider) - : base(database) - { - this.appProvider = appProvider; - - this.serializer = serializer; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel(Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.Status) - .Ascending(x => x.Id)), - new CreateIndexModel(Index - .Ascending(x => x.IndexedSchemaId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.Status) - .Ascending(x => x.Id)), - new CreateIndexModel(Index - .Ascending(x => x.ScheduledAt) - .Ascending(x => x.IsDeleted)), - new CreateIndexModel(Index - .Ascending(x => x.ReferencedIds)) - }, ct); - } - - protected override string CollectionName() - { - return "State_Contents"; - } - - public async Task> QueryAsync(ISchemaEntity schema, ClrQuery query, List ids, Status[] status, bool inDraft, bool includeDraft = true) - { - try - { - query = query.AdjustToModel(schema.SchemaDef, inDraft); - - var filter = query.ToFilter(schema.Id, ids, status); - - var contentCount = Collection.Find(filter).CountDocumentsAsync(); - var contentItems = - Collection.Find(filter) - .WithoutDraft(includeDraft) - .ContentTake(query) - .ContentSkip(query) - .ContentSort(query) - .ToListAsync(); - - await Task.WhenAll(contentItems, contentCount); - - foreach (var entity in contentItems.Result) - { - entity.ParseData(schema.SchemaDef, serializer); - } - - return ResultList.Create(contentCount.Result, contentItems.Result); - } - catch (MongoQueryException ex) - { - if (ex.Message.Contains("17406")) - { - throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items."); - } - else - { - throw; - } - } - } - - public async Task> QueryAsync(IAppEntity app, HashSet ids, Status[] status, bool includeDraft) - { - var find = Collection.Find(FilterFactory.IdsByApp(app.Id, ids, status)); - - var contentItems = await find.WithoutDraft(includeDraft).ToListAsync(); - - var schemaIds = contentItems.Select(x => x.IndexedSchemaId).ToList(); - var schemas = await Task.WhenAll(schemaIds.Select(x => appProvider.GetSchemaAsync(app.Id, x))); - - var result = new List<(IContentEntity Content, ISchemaEntity Schema)>(); - - foreach (var entity in contentItems) - { - var schema = schemas.FirstOrDefault(x => x.Id == entity.IndexedSchemaId); - - if (schema != null) - { - entity.ParseData(schema.SchemaDef, serializer); - - result.Add((entity, schema)); - } - } - - return result; - } - - public async Task> QueryAsync(ISchemaEntity schema, HashSet ids, Status[] status, bool includeDraft) - { - var find = Collection.Find(FilterFactory.IdsBySchema(schema.Id, ids, status)); - - var contentItems = await find.WithoutDraft(includeDraft).ToListAsync(); - - foreach (var entity in contentItems) - { - entity.ParseData(schema.SchemaDef, serializer); - } - - return ResultList.Create(contentItems.Count, contentItems); - } - - public async Task FindContentAsync(ISchemaEntity schema, Guid id, Status[] status, bool includeDraft) - { - var find = Collection.Find(FilterFactory.Build(schema.Id, id, status)); - - var contentEntity = await find.WithoutDraft(includeDraft).FirstOrDefaultAsync(); - - contentEntity?.ParseData(schema.SchemaDef, serializer); - - return contentEntity; - } - - public Task QueryScheduledWithoutDataAsync(Instant now, Func callback) - { - return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true) - .Not(x => x.DataByIds) - .Not(x => x.DataDraftByIds) - .ForEachAsync(c => - { - callback(c); - }); - } - - public async Task> QueryIdsAsync(ISchemaEntity schema, FilterNode filterNode) - { - var filter = filterNode.AdjustToModel(schema.SchemaDef, true).ToFilter(schema.Id); - - var contentEntities = - await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId) - .ToListAsync(); - - return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList(); - } - - public async Task> QueryIdsAsync(HashSet ids) - { - var contentEntities = - await Collection.Find(Filter.In(x => x.Id, ids)).Only(x => x.Id, x => x.IndexedSchemaId) - .ToListAsync(); - - return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList(); - } - - public async Task> QueryIdsAsync(Guid appId) - { - var contentEntities = - await Collection.Find(x => x.IndexedAppId == appId).Only(x => x.Id) - .ToListAsync(); - - return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); - } - - public async Task<(ContentState Value, long Version)> ReadAsync(Guid key, Func> getSchema) - { - var contentEntity = - await Collection.Find(x => x.Id == key) - .FirstOrDefaultAsync(); - - if (contentEntity != null) - { - var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); - - contentEntity.ParseData(schema.SchemaDef, serializer); - - return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); - } - - return (null, EtagVersion.NotFound); - } - - public Task ReadAllAsync(Func callback, Func> getSchema, CancellationToken ct = default) - { - return Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(async contentEntity => - { - var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); - - contentEntity.ParseData(schema.SchemaDef, serializer); - - await callback(SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); - }, ct); - } - - public Task CleanupAsync(Guid id) - { - return Collection.UpdateManyAsync( - Filter.And( - Filter.AnyEq(x => x.ReferencedIds, id), - Filter.AnyNe(x => x.ReferencedIdsDeleted, id)), - Update.AddToSet(x => x.ReferencedIdsDeleted, id)); - } - - public Task RemoveAsync(Guid id) - { - return Collection.DeleteOneAsync(x => x.Id == id); - } - - public async Task UpsertAsync(MongoContentEntity content, long oldVersion) - { - try - { - await Collection.ReplaceOneAsync(x => x.Id == content.Id && x.Version == oldVersion, content, Upsert); - } - catch (MongoWriteException ex) - { - if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) - { - var existingVersion = - await Collection.Find(x => x.Id == content.Id).Only(x => x.Id, x => x.Version) - .FirstOrDefaultAsync(); - - if (existingVersion != null) - { - throw new InconsistentStateException(existingVersion["vs"].AsInt64, oldVersion, ex); - } - } - else - { - throw; - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs deleted file mode 100644 index fe2e0649c..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ /dev/null @@ -1,133 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents -{ - public sealed class MongoContentEntity : IContentEntity - { - private NamedContentData data; - private NamedContentData dataDraft; - - [BsonId] - [BsonElement("_id")] - [BsonRepresentation(BsonType.String)] - public Guid Id { get; set; } - - [BsonRequired] - [BsonElement("_ai")] - [BsonRepresentation(BsonType.String)] - public Guid IndexedAppId { get; set; } - - [BsonRequired] - [BsonElement("_si")] - [BsonRepresentation(BsonType.String)] - public Guid IndexedSchemaId { get; set; } - - [BsonRequired] - [BsonElement("rf")] - [BsonRepresentation(BsonType.String)] - public List ReferencedIds { get; set; } - - [BsonRequired] - [BsonElement("rd")] - [BsonRepresentation(BsonType.String)] - public List ReferencedIdsDeleted { get; set; } = new List(); - - [BsonRequired] - [BsonElement("ss")] - public Status Status { get; set; } - - [BsonIgnoreIfNull] - [BsonElement("do")] - [BsonJson] - public IdContentData DataByIds { get; set; } - - [BsonIgnoreIfNull] - [BsonElement("dd")] - [BsonJson] - public IdContentData DataDraftByIds { get; set; } - - [BsonIgnoreIfNull] - [BsonElement("sj")] - [BsonJson] - public ScheduleJob ScheduleJob { get; set; } - - [BsonRequired] - [BsonElement("ai")] - public NamedId AppId { get; set; } - - [BsonRequired] - [BsonElement("si")] - public NamedId SchemaId { get; set; } - - [BsonIgnoreIfNull] - [BsonElement("sa")] - public Instant? ScheduledAt { get; set; } - - [BsonRequired] - [BsonElement("ct")] - public Instant Created { get; set; } - - [BsonRequired] - [BsonElement("mt")] - public Instant LastModified { get; set; } - - [BsonRequired] - [BsonElement("vs")] - public long Version { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement("dl")] - public bool IsDeleted { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement("pd")] - public bool IsPending { get; set; } - - [BsonRequired] - [BsonElement("cb")] - public RefToken CreatedBy { get; set; } - - [BsonRequired] - [BsonElement("mb")] - public RefToken LastModifiedBy { get; set; } - - [BsonIgnore] - public NamedContentData Data - { - get { return data; } - } - - [BsonIgnore] - public NamedContentData DataDraft - { - get { return dataDraft; } - } - - public void ParseData(Schema schema, IJsonSerializer serializer) - { - data = DataByIds.FromMongoModel(schema, ReferencedIdsDeleted, serializer); - - if (DataDraftByIds != null) - { - dataDraft = DataDraftByIds.FromMongoModel(schema, ReferencedIdsDeleted, serializer); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs deleted file mode 100644 index c7d13da72..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ /dev/null @@ -1,148 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Contents.Text; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents -{ - public partial class MongoContentRepository : IContentRepository, IInitializable - { - private readonly IAppProvider appProvider; - private readonly IJsonSerializer serializer; - private readonly ITextIndexer indexer; - private readonly string typeAssetDeleted; - private readonly string typeContentDeleted; - private readonly MongoContentCollection contents; - - static MongoContentRepository() - { - StatusSerializer.Register(); - } - - public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer, ITextIndexer indexer, TypeNameRegistry typeNameRegistry) - { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(serializer, nameof(serializer)); - Guard.NotNull(indexer, nameof(indexer)); - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); - - this.appProvider = appProvider; - this.indexer = indexer; - this.serializer = serializer; - - typeAssetDeleted = typeNameRegistry.GetName(); - typeContentDeleted = typeNameRegistry.GetName(); - - contents = new MongoContentCollection(database, serializer, appProvider); - } - - public Task InitializeAsync(CancellationToken ct = default) - { - return contents.InitializeAsync(ct); - } - - public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, ClrQuery query, bool includeDraft = true) - { - Guard.NotNull(app, nameof(app)); - Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(query, nameof(query)); - - using (Profiler.TraceMethod("QueryAsyncByQuery")) - { - var fullTextIds = await indexer.SearchAsync(query.FullText, app, schema.Id, inDraft ? Scope.Draft : Scope.Published); - - if (fullTextIds?.Count == 0) - { - return ResultList.CreateFrom(0); - } - - return await contents.QueryAsync(schema, query, fullTextIds, status, inDraft, includeDraft); - } - } - - public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids, bool includeDraft = true) - { - Guard.NotNull(app, nameof(app)); - Guard.NotNull(ids, nameof(ids)); - Guard.NotNull(schema, nameof(schema)); - - using (Profiler.TraceMethod("QueryAsyncByIds")) - { - return await contents.QueryAsync(schema, ids, status, includeDraft); - } - } - - public async Task> QueryAsync(IAppEntity app, Status[] status, HashSet ids, bool includeDraft = true) - { - Guard.NotNull(app, nameof(app)); - Guard.NotNull(ids, nameof(ids)); - - using (Profiler.TraceMethod("QueryAsyncByIdsWithoutSchema")) - { - return await contents.QueryAsync(app, ids, status, includeDraft); - } - } - - public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id, bool includeDraft = true) - { - Guard.NotNull(app, nameof(app)); - Guard.NotNull(schema, nameof(schema)); - - using (Profiler.TraceMethod()) - { - return await contents.FindContentAsync(schema, id, status, includeDraft); - } - } - - public async Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode) - { - using (Profiler.TraceMethod()) - { - return await contents.QueryIdsAsync(await appProvider.GetSchemaAsync(appId, schemaId), filterNode); - } - } - - public async Task> QueryIdsAsync(Guid appId, HashSet ids) - { - using (Profiler.TraceMethod()) - { - return await contents.QueryIdsAsync(ids); - } - } - - public async Task QueryScheduledWithoutDataAsync(Instant now, Func callback) - { - using (Profiler.TraceMethod()) - { - await contents.QueryScheduledWithoutDataAsync(now, callback); - } - } - - public Task ClearAsync() - { - return contents.ClearAsync(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs deleted file mode 100644 index 67aa7e213..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Contents.State; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents -{ - public partial class MongoContentRepository : ISnapshotStore - { - async Task ISnapshotStore.RemoveAsync(Guid key) - { - using (Profiler.TraceMethod()) - { - await contents.RemoveAsync(key); - } - } - - async Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct) - { - using (Profiler.TraceMethod()) - { - await contents.ReadAllAsync(callback, GetSchemaAsync, ct); - } - } - - async Task<(ContentState Value, long Version)> ISnapshotStore.ReadAsync(Guid key) - { - using (Profiler.TraceMethod()) - { - return await contents.ReadAsync(key, GetSchemaAsync); - } - } - - async Task ISnapshotStore.WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion) - { - using (Profiler.TraceMethod()) - { - if (value.SchemaId.Id == Guid.Empty) - { - return; - } - - var schema = await GetSchemaAsync(value.AppId.Id, value.SchemaId.Id); - - var idData = value.Data.ToMongoModel(schema.SchemaDef, serializer); - var idDraftData = idData; - - if (!ReferenceEquals(value.Data, value.DataDraft)) - { - idDraftData = value.DataDraft?.ToMongoModel(schema.SchemaDef, serializer); - } - - var content = SimpleMapper.Map(value, new MongoContentEntity - { - DataByIds = idData, - DataDraftByIds = idDraftData, - IsDeleted = value.IsDeleted, - IndexedAppId = value.AppId.Id, - IndexedSchemaId = value.SchemaId.Id, - ReferencedIds = idData.ToReferencedIds(schema.SchemaDef), - ScheduledAt = value.ScheduleJob?.DueTime, - Version = newVersion - }); - - await contents.UpsertAsync(content, oldVersion); - } - } - - private async Task GetSchemaAsync(Guid appId, Guid schemaId) - { - var schema = await appProvider.GetSchemaAsync(appId, schemaId, true); - - if (schema == null) - { - throw new DomainObjectNotFoundException(schemaId.ToString(), typeof(ISchemaEntity)); - } - - return schema; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs deleted file mode 100644 index c43523d61..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs +++ /dev/null @@ -1,139 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using MongoDB.Driver; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.MongoDb.Queries; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors -{ - public static class FilterFactory - { - private static readonly FilterDefinitionBuilder Filter = Builders.Filter; - - public static ClrQuery AdjustToModel(this ClrQuery query, Schema schema, bool useDraft) - { - var pathConverter = Adapt.Path(schema, useDraft); - - if (query.Filter != null) - { - query.Filter = query.Filter.Accept(new AdaptionVisitor(pathConverter)); - } - - query.Sort = query.Sort.Select(x => new SortNode(pathConverter(x.Path), x.Order)).ToList(); - - return query; - } - - public static FilterNode AdjustToModel(this FilterNode filterNode, Schema schema, bool useDraft) - { - var pathConverter = Adapt.Path(schema, useDraft); - - return filterNode.Accept(new AdaptionVisitor(pathConverter)); - } - - public static IFindFluent ContentSort(this IFindFluent cursor, ClrQuery query) - { - return cursor.Sort(query.BuildSort()); - } - - public static IFindFluent ContentTake(this IFindFluent cursor, ClrQuery query) - { - return cursor.Take(query); - } - - public static IFindFluent ContentSkip(this IFindFluent cursor, ClrQuery query) - { - return cursor.Skip(query); - } - - public static IFindFluent WithoutDraft(this IFindFluent cursor, bool includeDraft) - { - return !includeDraft ? cursor.Not(x => x.DataDraftByIds, x => x.IsDeleted) : cursor; - } - - public static FilterDefinition Build(Guid schemaId, Guid id, Status[] status) - { - return CreateFilter(null, schemaId, new List { id }, status, null); - } - - public static FilterDefinition IdsByApp(Guid appId, ICollection ids, Status[] status) - { - return CreateFilter(appId, null, ids, status, null); - } - - public static FilterDefinition IdsBySchema(Guid schemaId, ICollection ids, Status[] status) - { - return CreateFilter(null, schemaId, ids, status, null); - } - - public static FilterDefinition ToFilter(this ClrQuery query, Guid schemaId, ICollection ids, Status[] status) - { - return CreateFilter(null, schemaId, ids, status, query); - } - - private static FilterDefinition CreateFilter(Guid? appId, Guid? schemaId, ICollection ids, Status[] status, - ClrQuery query) - { - var filters = new List>(); - - if (appId.HasValue) - { - filters.Add(Filter.Eq(x => x.IndexedAppId, appId.Value)); - } - - if (schemaId.HasValue) - { - filters.Add(Filter.Eq(x => x.IndexedSchemaId, schemaId.Value)); - } - - filters.Add(Filter.Ne(x => x.IsDeleted, true)); - - if (status != null) - { - filters.Add(Filter.In(x => x.Status, status)); - } - - if (ids != null && ids.Count > 0) - { - if (ids.Count > 1) - { - filters.Add(Filter.In(x => x.Id, ids)); - } - else - { - filters.Add(Filter.Eq(x => x.Id, ids.First())); - } - } - - if (query?.Filter != null) - { - filters.Add(query.Filter.BuildFilter()); - } - - return Filter.And(filters); - } - - public static FilterDefinition ToFilter(this FilterNode filterNode, Guid schemaId) - { - var filters = new List> - { - Filter.Eq(x => x.IndexedSchemaId, schemaId), - Filter.Ne(x => x.IsDeleted, true), - filterNode.BuildFilter() - }; - - return Filter.And(filters); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs deleted file mode 100644 index 7f9c094f2..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules -{ - public sealed class MongoRuleEventEntity : MongoEntity, IRuleEventEntity - { - [BsonRequired] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public Guid AppId { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public Guid RuleId { get; set; } - - [BsonRequired] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public RuleResult Result { get; set; } - - [BsonRequired] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public RuleJobResult JobResult { get; set; } - - [BsonRequired] - [BsonElement] - [BsonJson] - public RuleJob Job { get; set; } - - [BsonRequired] - [BsonElement] - public string LastDump { get; set; } - - [BsonRequired] - [BsonElement] - public int NumCalls { get; set; } - - [BsonRequired] - [BsonElement] - public Instant Expires { get; set; } - - [BsonRequired] - [BsonElement] - public Instant? NextAttempt { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs deleted file mode 100644 index fed408874..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs +++ /dev/null @@ -1,136 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules -{ - public sealed class MongoRuleEventRepository : MongoRepositoryBase, IRuleEventRepository - { - private readonly MongoRuleStatisticsCollection statisticsCollection; - - public MongoRuleEventRepository(IMongoDatabase database) - : base(database) - { - statisticsCollection = new MongoRuleStatisticsCollection(database); - } - - protected override string CollectionName() - { - return "RuleEvents"; - } - - protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - await statisticsCollection.InitializeAsync(ct); - - await collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel(Index.Ascending(x => x.NextAttempt)), - new CreateIndexModel(Index.Ascending(x => x.AppId).Descending(x => x.Created)), - new CreateIndexModel( - Index - .Ascending(x => x.Expires), - new CreateIndexOptions - { - ExpireAfter = TimeSpan.Zero - }) - }, ct); - } - - public Task QueryPendingAsync(Instant now, Func callback, CancellationToken ct = default) - { - return Collection.Find(x => x.NextAttempt < now).ForEachAsync(callback, ct); - } - - public async Task> QueryByAppAsync(Guid appId, Guid? ruleId = null, int skip = 0, int take = 20) - { - var filter = Filter.Eq(x => x.AppId, appId); - - if (ruleId.HasValue) - { - filter = Filter.And(filter, Filter.Eq(x => x.RuleId, ruleId)); - } - - var ruleEventEntities = - await Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.Created) - .ToListAsync(); - - return ruleEventEntities; - } - - public async Task FindAsync(Guid id) - { - var ruleEvent = - await Collection.Find(x => x.Id == id) - .FirstOrDefaultAsync(); - - return ruleEvent; - } - - public async Task CountByAppAsync(Guid appId) - { - return (int)await Collection.CountDocumentsAsync(x => x.AppId == appId); - } - - public Task EnqueueAsync(Guid id, Instant nextAttempt) - { - return Collection.UpdateOneAsync(x => x.Id == id, Update.Set(x => x.NextAttempt, nextAttempt)); - } - - public Task EnqueueAsync(RuleJob job, Instant nextAttempt) - { - var entity = SimpleMapper.Map(job, new MongoRuleEventEntity { Job = job, Created = nextAttempt, NextAttempt = nextAttempt }); - - return Collection.InsertOneIfNotExistsAsync(entity); - } - - public Task CancelAsync(Guid id) - { - return Collection.UpdateOneAsync(x => x.Id == id, - Update - .Set(x => x.NextAttempt, null) - .Set(x => x.JobResult, RuleJobResult.Cancelled)); - } - - public async Task MarkSentAsync(RuleJob job, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant finished, Instant? nextCall) - { - if (result == RuleResult.Success) - { - await statisticsCollection.IncrementSuccess(job.AppId, job.RuleId, finished); - } - else - { - await statisticsCollection.IncrementFailed(job.AppId, job.RuleId, finished); - } - - await Collection.UpdateOneAsync(x => x.Id == job.Id, - Update - .Set(x => x.Result, result) - .Set(x => x.LastDump, dump) - .Set(x => x.JobResult, jobResult) - .Set(x => x.NextAttempt, nextCall) - .Inc(x => x.NumCalls, 1)); - } - - public Task> QueryStatisticsByAppAsync(Guid appId) - { - return statisticsCollection.QueryByAppAsync(appId); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj deleted file mode 100644 index 1b63c901a..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs deleted file mode 100644 index f4b76508f..000000000 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ /dev/null @@ -1,121 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Indexes; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Rules.Indexes; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Indexes; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Caching; -using Squidex.Infrastructure.Security; - -namespace Squidex.Domain.Apps.Entities -{ - public sealed class AppProvider : IAppProvider - { - private readonly ILocalCache localCache; - private readonly IAppsIndex indexForApps; - private readonly IRulesIndex indexRules; - private readonly ISchemasIndex indexSchemas; - - public AppProvider(ILocalCache localCache, IAppsIndex indexForApps, IRulesIndex indexRules, ISchemasIndex indexSchemas) - { - Guard.NotNull(indexForApps, nameof(indexForApps)); - Guard.NotNull(indexRules, nameof(indexRules)); - Guard.NotNull(indexSchemas, nameof(indexSchemas)); - Guard.NotNull(localCache, nameof(localCache)); - - this.localCache = localCache; - this.indexForApps = indexForApps; - this.indexRules = indexRules; - this.indexSchemas = indexSchemas; - } - - public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) - { - return localCache.GetOrCreateAsync($"GetAppWithSchemaAsync({appId}, {id})", async () => - { - var app = await GetAppAsync(appId); - - if (app == null) - { - return (null, null); - } - - var schema = await GetSchemaAsync(appId, id, false); - - if (schema == null) - { - return (null, null); - } - - return (app, schema); - }); - } - - public Task GetAppAsync(Guid appId) - { - return localCache.GetOrCreateAsync($"GetAppAsync({appId})", async () => - { - return await indexForApps.GetAppAsync(appId); - }); - } - - public Task GetAppAsync(string appName) - { - return localCache.GetOrCreateAsync($"GetAppAsync({appName})", async () => - { - return await indexForApps.GetAppByNameAsync(appName); - }); - } - - public Task> GetUserAppsAsync(string userId, PermissionSet permissions) - { - return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () => - { - return await indexForApps.GetAppsForUserAsync(userId, permissions); - }); - } - - public Task GetSchemaAsync(Guid appId, string name) - { - return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {name})", async () => - { - return await indexSchemas.GetSchemaByNameAsync(appId, name); - }); - } - - public Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) - { - return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {id}, {allowDeleted})", async () => - { - return await indexSchemas.GetSchemaAsync(appId, id, allowDeleted); - }); - } - - public Task> GetSchemasAsync(Guid appId) - { - return localCache.GetOrCreateAsync($"GetSchemasAsync({appId})", async () => - { - return await indexSchemas.GetSchemasAsync(appId); - }); - } - - public Task> GetRulesAsync(Guid appId) - { - return localCache.GetOrCreateAsync($"GetRulesAsync({appId})", async () => - { - return await indexRules.GetRulesAsync(appId); - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs deleted file mode 100644 index f1caac4dc..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class AppCommandMiddleware : GrainCommandMiddleware - { - private readonly IAssetStore assetStore; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - private readonly IContextProvider contextProvider; - - public AppCommandMiddleware( - IGrainFactory grainFactory, - IAssetStore assetStore, - IAssetThumbnailGenerator assetThumbnailGenerator, - IContextProvider contextProvider) - : base(grainFactory) - { - Guard.NotNull(contextProvider, nameof(contextProvider)); - Guard.NotNull(assetStore, nameof(assetStore)); - Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); - - this.assetStore = assetStore; - this.assetThumbnailGenerator = assetThumbnailGenerator; - this.contextProvider = contextProvider; - } - - public override async Task HandleAsync(CommandContext context, Func next) - { - if (context.Command is UploadAppImage uploadImage) - { - await UploadAsync(uploadImage); - } - - await ExecuteCommandAsync(context); - - if (context.PlainResult is IAppEntity app) - { - contextProvider.Context.App = app; - } - - await next(); - } - - private async Task UploadAsync(UploadAppImage uploadImage) - { - var file = uploadImage.File; - - var image = await assetThumbnailGenerator.GetImageInfoAsync(file.OpenRead()); - - if (image == null) - { - throw new ValidationException("File is not an image."); - } - - await assetStore.UploadAsync(uploadImage.AppId.ToString(), file.OpenRead(), true); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs deleted file mode 100644 index b7ab61cf5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs +++ /dev/null @@ -1,510 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Guards; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Domain.Apps.Entities.Apps.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class AppGrain : DomainObjectGrain, IAppGrain - { - private readonly InitialPatterns initialPatterns; - private readonly IAppPlansProvider appPlansProvider; - private readonly IAppPlanBillingManager appPlansBillingManager; - private readonly IUserResolver userResolver; - - public AppGrain( - InitialPatterns initialPatterns, - IStore store, - ISemanticLog log, - IAppPlansProvider appPlansProvider, - IAppPlanBillingManager appPlansBillingManager, - IUserResolver userResolver) - : base(store, log) - { - Guard.NotNull(initialPatterns, nameof(initialPatterns)); - Guard.NotNull(userResolver, nameof(userResolver)); - Guard.NotNull(appPlansProvider, nameof(appPlansProvider)); - Guard.NotNull(appPlansBillingManager, nameof(appPlansBillingManager)); - - this.userResolver = userResolver; - this.appPlansProvider = appPlansProvider; - this.appPlansBillingManager = appPlansBillingManager; - this.initialPatterns = initialPatterns; - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - VerifyNotArchived(); - - switch (command) - { - case CreateApp createApp: - return CreateReturn(createApp, c => - { - GuardApp.CanCreate(c); - - Create(c); - - return Snapshot; - }); - - case UpdateApp updateApp: - return UpdateReturn(updateApp, c => - { - GuardApp.CanUpdate(c); - - Update(c); - - return Snapshot; - }); - - case UploadAppImage uploadImage: - return UpdateReturn(uploadImage, c => - { - GuardApp.CanUploadImage(c); - - UploadImage(c); - - return Snapshot; - }); - - case RemoveAppImage removeImage: - return UpdateReturn(removeImage, c => - { - GuardApp.CanRemoveImage(c); - - RemoveImage(c); - - return Snapshot; - }); - - case AssignContributor assignContributor: - return UpdateReturnAsync(assignContributor, async c => - { - await GuardAppContributors.CanAssign(Snapshot.Contributors, Snapshot.Roles, c, userResolver, GetPlan()); - - AssignContributor(c, !Snapshot.Contributors.ContainsKey(assignContributor.ContributorId)); - - return Snapshot; - }); - - case RemoveContributor removeContributor: - return UpdateReturn(removeContributor, c => - { - GuardAppContributors.CanRemove(Snapshot.Contributors, c); - - RemoveContributor(c); - - return Snapshot; - }); - - case AttachClient attachClient: - return UpdateReturn(attachClient, c => - { - GuardAppClients.CanAttach(Snapshot.Clients, c); - - AttachClient(c); - - return Snapshot; - }); - - case UpdateClient updateClient: - return UpdateReturn(updateClient, c => - { - GuardAppClients.CanUpdate(Snapshot.Clients, c, Snapshot.Roles); - - UpdateClient(c); - - return Snapshot; - }); - - case RevokeClient revokeClient: - return UpdateReturn(revokeClient, c => - { - GuardAppClients.CanRevoke(Snapshot.Clients, c); - - RevokeClient(c); - - return Snapshot; - }); - - case AddWorkflow addWorkflow: - return UpdateReturn(addWorkflow, c => - { - GuardAppWorkflows.CanAdd(c); - - AddWorkflow(c); - - return Snapshot; - }); - - case UpdateWorkflow updateWorkflow: - return UpdateReturn(updateWorkflow, c => - { - GuardAppWorkflows.CanUpdate(Snapshot.Workflows, c); - - UpdateWorkflow(c); - - return Snapshot; - }); - - case DeleteWorkflow deleteWorkflow: - return UpdateReturn(deleteWorkflow, c => - { - GuardAppWorkflows.CanDelete(Snapshot.Workflows, c); - - DeleteWorkflow(c); - - return Snapshot; - }); - - case AddLanguage addLanguage: - return UpdateReturn(addLanguage, c => - { - GuardAppLanguages.CanAdd(Snapshot.LanguagesConfig, c); - - AddLanguage(c); - - return Snapshot; - }); - - case RemoveLanguage removeLanguage: - return UpdateReturn(removeLanguage, c => - { - GuardAppLanguages.CanRemove(Snapshot.LanguagesConfig, c); - - RemoveLanguage(c); - - return Snapshot; - }); - - case UpdateLanguage updateLanguage: - return UpdateReturn(updateLanguage, c => - { - GuardAppLanguages.CanUpdate(Snapshot.LanguagesConfig, c); - - UpdateLanguage(c); - - return Snapshot; - }); - - case AddRole addRole: - return UpdateReturn(addRole, c => - { - GuardAppRoles.CanAdd(Snapshot.Roles, c); - - AddRole(c); - - return Snapshot; - }); - - case DeleteRole deleteRole: - return UpdateReturn(deleteRole, c => - { - GuardAppRoles.CanDelete(Snapshot.Roles, c, Snapshot.Contributors, Snapshot.Clients); - - DeleteRole(c); - - return Snapshot; - }); - - case UpdateRole updateRole: - return UpdateReturn(updateRole, c => - { - GuardAppRoles.CanUpdate(Snapshot.Roles, c); - - UpdateRole(c); - - return Snapshot; - }); - - case AddPattern addPattern: - return UpdateReturn(addPattern, c => - { - GuardAppPatterns.CanAdd(Snapshot.Patterns, c); - - AddPattern(c); - - return Snapshot; - }); - - case DeletePattern deletePattern: - return UpdateReturn(deletePattern, c => - { - GuardAppPatterns.CanDelete(Snapshot.Patterns, c); - - DeletePattern(c); - - return Snapshot; - }); - - case UpdatePattern updatePattern: - return UpdateReturn(updatePattern, c => - { - GuardAppPatterns.CanUpdate(Snapshot.Patterns, c); - - UpdatePattern(c); - - return Snapshot; - }); - - case ChangePlan changePlan: - return UpdateReturnAsync(changePlan, async c => - { - GuardApp.CanChangePlan(c, Snapshot.Plan, appPlansProvider); - - if (c.FromCallback) - { - ChangePlan(c); - - return null; - } - else - { - var result = await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.NamedId(), c.PlanId); - - switch (result) - { - case PlanChangedResult _: - ChangePlan(c); - break; - case PlanResetResult _: - ResetPlan(c); - break; - } - - return result; - } - }); - - case ArchiveApp archiveApp: - return UpdateAsync(archiveApp, async c => - { - await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.NamedId(), null); - - ArchiveApp(c); - }); - - default: - throw new NotSupportedException(); - } - } - - private IAppLimitsPlan GetPlan() - { - return appPlansProvider.GetPlan(Snapshot.Plan?.PlanId); - } - - public void Create(CreateApp command) - { - var appId = NamedId.Of(command.AppId, command.Name); - - var events = new List - { - CreateInitalEvent(command.Name), - CreateInitialOwner(command.Actor), - CreateInitialLanguage() - }; - - foreach (var pattern in initialPatterns) - { - events.Add(CreateInitialPattern(pattern.Key, pattern.Value)); - } - - foreach (var @event in events) - { - @event.Actor = command.Actor; - @event.AppId = appId; - - RaiseEvent(@event); - } - } - - public void UpdateClient(UpdateClient command) - { - if (!string.IsNullOrWhiteSpace(command.Name)) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed())); - } - - if (command.Role != null) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Role = command.Role })); - } - } - - public void Update(UpdateApp command) - { - RaiseEvent(SimpleMapper.Map(command, new AppUpdated())); - } - - public void UploadImage(UploadAppImage command) - { - RaiseEvent(SimpleMapper.Map(command, new AppImageUploaded { Image = new AppImage(command.File.MimeType) })); - } - - public void RemoveImage(RemoveAppImage command) - { - RaiseEvent(SimpleMapper.Map(command, new AppImageRemoved())); - } - - public void UpdateLanguage(UpdateLanguage command) - { - RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated())); - } - - public void AssignContributor(AssignContributor command, bool isAdded) - { - RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned { IsAdded = isAdded })); - } - - public void RemoveContributor(RemoveContributor command) - { - RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved())); - } - - public void AttachClient(AttachClient command) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientAttached())); - } - - public void RevokeClient(RevokeClient command) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked())); - } - - public void AddWorkflow(AddWorkflow command) - { - RaiseEvent(SimpleMapper.Map(command, new AppWorkflowAdded())); - } - - public void UpdateWorkflow(UpdateWorkflow command) - { - RaiseEvent(SimpleMapper.Map(command, new AppWorkflowUpdated())); - } - - public void DeleteWorkflow(DeleteWorkflow command) - { - RaiseEvent(SimpleMapper.Map(command, new AppWorkflowDeleted())); - } - - public void AddLanguage(AddLanguage command) - { - RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded())); - } - - public void RemoveLanguage(RemoveLanguage command) - { - RaiseEvent(SimpleMapper.Map(command, new AppLanguageRemoved())); - } - - public void ChangePlan(ChangePlan command) - { - RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged())); - } - - public void ResetPlan(ChangePlan command) - { - RaiseEvent(SimpleMapper.Map(command, new AppPlanReset())); - } - - public void AddPattern(AddPattern command) - { - RaiseEvent(SimpleMapper.Map(command, new AppPatternAdded())); - } - - public void DeletePattern(DeletePattern command) - { - RaiseEvent(SimpleMapper.Map(command, new AppPatternDeleted())); - } - - public void UpdatePattern(UpdatePattern command) - { - RaiseEvent(SimpleMapper.Map(command, new AppPatternUpdated())); - } - - public void AddRole(AddRole command) - { - RaiseEvent(SimpleMapper.Map(command, new AppRoleAdded())); - } - - public void DeleteRole(DeleteRole command) - { - RaiseEvent(SimpleMapper.Map(command, new AppRoleDeleted())); - } - - public void UpdateRole(UpdateRole command) - { - RaiseEvent(SimpleMapper.Map(command, new AppRoleUpdated())); - } - - public void ArchiveApp(ArchiveApp command) - { - RaiseEvent(SimpleMapper.Map(command, new AppArchived())); - } - - private void VerifyNotArchived() - { - if (Snapshot.IsArchived) - { - throw new DomainException("App has already been archived."); - } - } - - private void RaiseEvent(AppEvent @event) - { - if (@event.AppId == null) - { - @event.AppId = NamedId.Of(Snapshot.Id, Snapshot.Name); - } - - RaiseEvent(Envelope.Create(@event)); - } - - private static AppCreated CreateInitalEvent(string name) - { - return new AppCreated { Name = name }; - } - - private static AppPatternAdded CreateInitialPattern(Guid id, AppPattern pattern) - { - return new AppPatternAdded { PatternId = id, Name = pattern.Name, Pattern = pattern.Pattern, Message = pattern.Message }; - } - - private static AppLanguageAdded CreateInitialLanguage() - { - return new AppLanguageAdded { Language = Language.EN }; - } - - private static AppContributorAssigned CreateInitialOwner(RefToken actor) - { - return new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }; - } - - public Task> GetStateAsync() - { - return J.AsTask(Snapshot); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs deleted file mode 100644 index d52434575..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs +++ /dev/null @@ -1,161 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.History; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public class AppHistoryEventsCreator : HistoryEventsCreatorBase - { - public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry) - : base(typeNameRegistry) - { - AddEventMessage( - "assigned {user:[Contributor]} as {[Role]}"); - - AddEventMessage( - "removed {user:[Contributor]} from app"); - - AddEventMessage( - "added client {[Id]} to app"); - - AddEventMessage( - "revoked client {[Id]}"); - - AddEventMessage( - "updated client {[Id]}"); - - AddEventMessage( - "renamed client {[Id]} to {[Name]}"); - - AddEventMessage( - "changed plan to {[Plan]}"); - - AddEventMessage( - "resetted plan"); - - AddEventMessage( - "added language {[Language]}"); - - AddEventMessage( - "removed language {[Language]}"); - - AddEventMessage( - "updated language {[Language]}"); - - AddEventMessage( - "changed master language to {[Language]}"); - - AddEventMessage( - "added pattern {[Name]}"); - - AddEventMessage( - "deleted pattern {[PatternId]}"); - - AddEventMessage( - "updated pattern {[Name]}"); - - AddEventMessage( - "added role {[Name]}"); - - AddEventMessage( - "deleted role {[Name]}"); - - AddEventMessage( - "updated role {[Name]}"); - } - - private HistoryEvent CreateEvent(IEvent @event) - { - switch (@event) - { - case AppContributorAssigned e: - return CreateContributorsEvent(e, e.ContributorId, e.Role); - case AppContributorRemoved e: - return CreateContributorsEvent(e, e.ContributorId); - case AppClientAttached e: - return CreateClientsEvent(e, e.Id); - case AppClientRenamed e: - return CreateClientsEvent(e, e.Id, ClientName(e)); - case AppClientRevoked e: - return CreateClientsEvent(e, e.Id); - case AppLanguageAdded e: - return CreateLanguagesEvent(e, e.Language); - case AppLanguageUpdated e: - return CreateLanguagesEvent(e, e.Language); - case AppMasterLanguageSet e: - return CreateLanguagesEvent(e, e.Language); - case AppLanguageRemoved e: - return CreateLanguagesEvent(e, e.Language); - case AppPatternAdded e: - return CreatePatternsEvent(e, e.PatternId, e.Name); - case AppPatternUpdated e: - return CreatePatternsEvent(e, e.PatternId, e.Name); - case AppPatternDeleted e: - return CreatePatternsEvent(e, e.PatternId); - case AppRoleAdded e: - return CreateRolesEvent(e, e.Name); - case AppRoleUpdated e: - return CreateRolesEvent(e, e.Name); - case AppRoleDeleted e: - return CreateRolesEvent(e, e.Name); - case AppPlanChanged e: - return CreatePlansEvent(e, e.PlanId); - case AppPlanReset e: - return CreatePlansEvent(e); - } - - return null; - } - - private HistoryEvent CreateContributorsEvent(IEvent e, string contributor, string role = null) - { - return ForEvent(e, "settings.contributors").Param("Contributor", contributor).Param("Role", role); - } - - private HistoryEvent CreateLanguagesEvent(IEvent e, Language language) - { - return ForEvent(e, "settings.languages").Param("Language", language); - } - - private HistoryEvent CreateRolesEvent(IEvent e, string name) - { - return ForEvent(e, "settings.roles").Param("Name", name); - } - - private HistoryEvent CreatePatternsEvent(IEvent e, Guid id, string name = null) - { - return ForEvent(e, "settings.patterns").Param("PatternId", id).Param("Name", name); - } - - private HistoryEvent CreateClientsEvent(IEvent e, string id, string name = null) - { - return ForEvent(e, "settings.clients").Param("Id", id).Param("Name", name); - } - - private HistoryEvent CreatePlansEvent(IEvent e, string plan = null) - { - return ForEvent(e, "settings.plan").Param("Plan", plan); - } - - protected override Task CreateEventCoreAsync(Envelope @event) - { - return Task.FromResult(CreateEvent(@event.Payload)); - } - - private static string ClientName(AppClientRenamed e) - { - return !string.IsNullOrWhiteSpace(e.Name) ? e.Name : e.Id; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs deleted file mode 100644 index e9182b39d..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class AppUISettings : IAppUISettings - { - private readonly IGrainFactory grainFactory; - - public AppUISettings(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public async Task GetAsync(Guid appId, string userId) - { - var result = await GetGrain(appId, userId).GetAsync(); - - return result.Value; - } - - public Task RemoveAsync(Guid appId, string userId, string path) - { - return GetGrain(appId, userId).RemoveAsync(path); - } - - public Task SetAsync(Guid appId, string userId, string path, IJsonValue value) - { - return GetGrain(appId, userId).SetAsync(path, value.AsJ()); - } - - public Task SetAsync(Guid appId, string userId, JsonObject settings) - { - return GetGrain(appId, userId).SetAsync(settings.AsJ()); - } - - private IAppUISettingsGrain GetGrain(Guid appId, string userId) - { - return grainFactory.GetGrain(Key(appId, userId)); - } - - private string Key(Guid appId, string userId) - { - if (!string.IsNullOrWhiteSpace(userId)) - { - return $"{appId}_{userId}"; - } - else - { - return $"{appId}"; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs deleted file mode 100644 index 519afc570..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs +++ /dev/null @@ -1,115 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class AppUISettingsGrain : GrainOfString, IAppUISettingsGrain - { - private readonly IGrainState state; - - [CollectionName("UISettings")] - public sealed class GrainState - { - public JsonObject Settings { get; set; } = JsonValue.Object(); - } - - public AppUISettingsGrain(IGrainState state) - { - Guard.NotNull(state, nameof(state)); - - this.state = state; - } - - public Task> GetAsync() - { - return Task.FromResult(state.Value.Settings.AsJ()); - } - - public Task SetAsync(J settings) - { - state.Value.Settings = settings; - - return state.WriteAsync(); - } - - public Task SetAsync(string path, J value) - { - var container = GetContainer(path, true, out var key); - - if (container == null) - { - throw new InvalidOperationException("Path does not lead to an object."); - } - - container[key] = value.Value; - - return state.WriteAsync(); - } - - public async Task RemoveAsync(string path) - { - var container = GetContainer(path, false, out var key); - - if (container?.ContainsKey(key) == true) - { - container.Remove(key); - - await state.WriteAsync(); - } - } - - private JsonObject GetContainer(string path, bool add, out string key) - { - Guard.NotNullOrEmpty(path, nameof(path)); - - var segments = path.Split('.'); - - key = segments[segments.Length - 1]; - - var current = state.Value.Settings; - - if (segments.Length > 1) - { - foreach (var segment in segments.Take(segments.Length - 1)) - { - if (!current.TryGetValue(segment, out var temp)) - { - if (add) - { - temp = JsonValue.Object(); - - current[segment] = temp; - } - else - { - return null; - } - } - - if (temp is JsonObject next) - { - current = next; - } - else - { - return null; - } - } - } - - return current; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs b/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs deleted file mode 100644 index a137a0c72..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs +++ /dev/null @@ -1,201 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps.Indexes; -using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class BackupApps : BackupHandler - { - private const string UsersFile = "Users.json"; - private const string SettingsFile = "Settings.json"; - private readonly IAppUISettings appUISettings; - private readonly IAppsIndex appsIndex; - private readonly IUserResolver userResolver; - private readonly HashSet contributors = new HashSet(); - private readonly Dictionary userMapping = new Dictionary(); - private Dictionary usersWithEmail = new Dictionary(); - private string appReservation; - private string appName; - - public override string Name { get; } = "Apps"; - - public BackupApps(IAppUISettings appUISettings, IAppsIndex appsIndex, IUserResolver userResolver) - { - Guard.NotNull(appsIndex, nameof(appsIndex)); - Guard.NotNull(appUISettings, nameof(appUISettings)); - Guard.NotNull(userResolver, nameof(userResolver)); - - this.appsIndex = appsIndex; - this.appUISettings = appUISettings; - this.userResolver = userResolver; - } - - public override async Task BackupEventAsync(Envelope @event, Guid appId, BackupWriter writer) - { - if (@event.Payload is AppContributorAssigned appContributorAssigned) - { - var userId = appContributorAssigned.ContributorId; - - if (!usersWithEmail.ContainsKey(userId)) - { - var user = await userResolver.FindByIdOrEmailAsync(userId); - - if (user != null) - { - usersWithEmail.Add(userId, user.Email); - } - } - } - } - - public override async Task BackupAsync(Guid appId, BackupWriter writer) - { - await WriteUsersAsync(writer); - await WriteSettingsAsync(writer, appId); - } - - public override async Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) - { - switch (@event.Payload) - { - case AppCreated appCreated: - { - appName = appCreated.Name; - - await ResolveUsersAsync(reader); - await ReserveAppAsync(appId); - - break; - } - - case AppContributorAssigned contributorAssigned: - { - if (!userMapping.TryGetValue(contributorAssigned.ContributorId, out var user) || user.Equals(actor)) - { - return false; - } - - contributorAssigned.ContributorId = user.Identifier; - contributors.Add(contributorAssigned.ContributorId); - break; - } - - case AppContributorRemoved contributorRemoved: - { - if (!userMapping.TryGetValue(contributorRemoved.ContributorId, out var user) || user.Equals(actor)) - { - return false; - } - - contributorRemoved.ContributorId = user.Identifier; - contributors.Remove(contributorRemoved.ContributorId); - break; - } - } - - if (@event.Payload is SquidexEvent squidexEvent) - { - squidexEvent.Actor = MapUser(squidexEvent.Actor.Identifier, actor); - } - - return true; - } - - public override Task RestoreAsync(Guid appId, BackupReader reader) - { - return ReadSettingsAsync(reader, appId); - } - - private async Task ReserveAppAsync(Guid appId) - { - appReservation = await appsIndex.ReserveAsync(appId, appName); - - if (appReservation == null) - { - throw new BackupRestoreException("The app id or name is not available."); - } - } - - public override async Task CleanupRestoreErrorAsync(Guid appId) - { - await appsIndex.RemoveReservationAsync(appReservation); - } - - private RefToken MapUser(string userId, RefToken fallback) - { - return userMapping.GetOrAdd(userId, fallback); - } - - private async Task ResolveUsersAsync(BackupReader reader) - { - await ReadUsersAsync(reader); - - foreach (var kvp in usersWithEmail) - { - var email = kvp.Value; - - var user = await userResolver.FindByIdOrEmailAsync(email); - - if (user == null && await userResolver.CreateUserIfNotExists(kvp.Value)) - { - user = await userResolver.FindByIdOrEmailAsync(email); - } - - if (user != null) - { - userMapping[kvp.Key] = new RefToken(RefTokenType.Subject, user.Id); - } - } - } - - private async Task ReadUsersAsync(BackupReader reader) - { - var json = await reader.ReadJsonAttachmentAsync>(UsersFile); - - usersWithEmail = json; - } - - private async Task WriteUsersAsync(BackupWriter writer) - { - var json = usersWithEmail; - - await writer.WriteJsonAsync(UsersFile, json); - } - - private async Task WriteSettingsAsync(BackupWriter writer, Guid appId) - { - var json = await appUISettings.GetAsync(appId, null); - - await writer.WriteJsonAsync(SettingsFile, json); - } - - private async Task ReadSettingsAsync(BackupReader reader, Guid appId) - { - var json = await reader.ReadJsonAttachmentAsync(SettingsFile); - - await appUISettings.SetAsync(appId, null, json); - } - - public override async Task CompleteRestoreAsync(Guid appId, BackupReader reader) - { - await appsIndex.AddAsync(appReservation); - - await appsIndex.RebuildByContributorsAsync(appId, contributors); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs deleted file mode 100644 index 30873adbb..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Entities.Apps.Commands -{ - public sealed class AddPattern : AppCommand - { - public Guid PatternId { get; set; } - - public string Name { get; set; } - - public string Pattern { get; set; } - - public string Message { get; set; } - - public AddPattern() - { - PatternId = Guid.NewGuid(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs deleted file mode 100644 index 8b5f0ef32..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Entities.Apps.Commands -{ - public sealed class UpdateApp : AppCommand - { - public string Label { get; set; } - - public string Description { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs deleted file mode 100644 index 415856189..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Entities.Apps.Commands -{ - public sealed class UpdatePattern : AppCommand - { - public Guid PatternId { get; set; } - - public string Name { get; set; } - - public string Pattern { get; set; } - - public string Message { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs b/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs deleted file mode 100644 index 4977a743e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class DefaultAppLogStore : IAppLogStore - { - private readonly ILogStore logStore; - - public DefaultAppLogStore(ILogStore logStore) - { - Guard.NotNull(logStore, nameof(logStore)); - - this.logStore = logStore; - } - - public Task ReadLogAsync(string appId, DateTime from, DateTime to, Stream stream) - { - Guard.NotNull(appId, nameof(appId)); - - return logStore.ReadLogAsync(appId, from, to, stream); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs b/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs deleted file mode 100644 index 729ef1441..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Orleans; -using Squidex.Domain.Apps.Entities.Apps.Indexes; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics -{ - public sealed class OrleansAppsHealthCheck : IHealthCheck - { - private readonly IAppsByNameIndexGrain index; - - public OrleansAppsHealthCheck(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - index = grainFactory.GetGrain(SingleGrain.Id); - } - - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - await index.CountAsync(); - - return HealthCheckResult.Healthy("Orleans must establish communication."); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs deleted file mode 100644 index 4360e60f4..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs +++ /dev/null @@ -1,84 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public static class GuardApp - { - public static void CanCreate(CreateApp command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot create app.", e => - { - if (!command.Name.IsSlug()) - { - e(Not.ValidSlug("Name"), nameof(command.Name)); - } - }); - } - - public static void CanUploadImage(UploadAppImage command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot upload image.", e => - { - if (command.File == null) - { - e(Not.Defined("File"), nameof(command.File)); - } - }); - } - - public static void CanUpdate(UpdateApp command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanRemoveImage(RemoveAppImage command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanChangePlan(ChangePlan command, AppPlan plan, IAppPlansProvider appPlans) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot change plan.", e => - { - if (string.IsNullOrWhiteSpace(command.PlanId)) - { - e(Not.Defined("Plan id"), nameof(command.PlanId)); - return; - } - - if (appPlans.GetPlan(command.PlanId) == null) - { - e("A plan with this id does not exist.", nameof(command.PlanId)); - } - - if (!string.IsNullOrWhiteSpace(command.PlanId) && plan != null && !plan.Owner.Equals(command.Actor)) - { - e("Plan can only changed from the user who configured the plan initially."); - } - - if (string.Equals(command.PlanId, plan?.PlanId, StringComparison.OrdinalIgnoreCase)) - { - e("App has already this plan."); - } - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs deleted file mode 100644 index 3334518c5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs +++ /dev/null @@ -1,104 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public static class GuardAppClients - { - public static void CanAttach(AppClients clients, AttachClient command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot attach client.", e => - { - if (string.IsNullOrWhiteSpace(command.Id)) - { - e(Not.Defined("Client id"), nameof(command.Id)); - } - else if (clients.ContainsKey(command.Id)) - { - e("A client with the same id already exists."); - } - }); - } - - public static void CanRevoke(AppClients clients, RevokeClient command) - { - Guard.NotNull(command, nameof(command)); - - GetClientOrThrow(clients, command.Id); - - Validate.It(() => "Cannot revoke client.", e => - { - if (string.IsNullOrWhiteSpace(command.Id)) - { - e(Not.Defined("Client id"), nameof(command.Id)); - } - }); - } - - public static void CanUpdate(AppClients clients, UpdateClient command, Roles roles) - { - Guard.NotNull(command, nameof(command)); - - var client = GetClientOrThrow(clients, command.Id); - - Validate.It(() => "Cannot update client.", e => - { - if (string.IsNullOrWhiteSpace(command.Id)) - { - e(Not.Defined("Client id"), nameof(command.Id)); - } - - if (string.IsNullOrWhiteSpace(command.Name) && command.Role == null) - { - e(Not.DefinedOr("name", "role"), nameof(command.Name), nameof(command.Role)); - } - - if (command.Role != null && !roles.Contains(command.Role)) - { - e(Not.Valid("role"), nameof(command.Role)); - } - - if (client == null) - { - return; - } - - if (!string.IsNullOrWhiteSpace(command.Name) && string.Equals(client.Name, command.Name)) - { - e(Not.New("Client", "name"), nameof(command.Name)); - } - - if (command.Role == client.Role) - { - e(Not.New("Client", "role"), nameof(command.Role)); - } - }); - } - - private static AppClient GetClientOrThrow(AppClients clients, string id) - { - if (string.IsNullOrWhiteSpace(id)) - { - return null; - } - - if (!clients.TryGetValue(id, out var client)) - { - throw new DomainObjectNotFoundException(id, "Clients", typeof(IAppEntity)); - } - - return client; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs deleted file mode 100644 index 120b0d44d..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs +++ /dev/null @@ -1,99 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public static class GuardAppContributors - { - public static Task CanAssign(AppContributors contributors, Roles roles, AssignContributor command, IUserResolver users, IAppLimitsPlan plan) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot assign contributor.", async e => - { - if (!roles.Contains(command.Role)) - { - e(Not.Valid("role"), nameof(command.Role)); - } - - if (string.IsNullOrWhiteSpace(command.ContributorId)) - { - e(Not.Defined("Contributor id"), nameof(command.ContributorId)); - } - else - { - var user = await users.FindByIdOrEmailAsync(command.ContributorId); - - if (user == null) - { - throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity)); - } - - command.ContributorId = user.Id; - - if (!command.IsRestore) - { - if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase)) - { - throw new DomainForbiddenException("You cannot change your own role."); - } - - if (contributors.TryGetValue(command.ContributorId, out var role)) - { - if (role == command.Role) - { - e(Not.New("Contributor", "role"), nameof(command.Role)); - } - } - else - { - if (plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors) - { - e("You have reached the maximum number of contributors for your plan."); - } - } - } - } - }); - } - - public static void CanRemove(AppContributors contributors, RemoveContributor command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot remove contributor.", e => - { - if (string.IsNullOrWhiteSpace(command.ContributorId)) - { - e(Not.Defined("Contributor id"), nameof(command.ContributorId)); - } - - var ownerIds = contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToList(); - - if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId)) - { - e("Cannot remove the only owner."); - } - }); - - if (!contributors.ContainsKey(command.ContributorId)) - { - throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity)); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs deleted file mode 100644 index a8a197271..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public static class GuardAppLanguages - { - public static void CanAdd(LanguagesConfig languages, AddLanguage command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot add language.", e => - { - if (command.Language == null) - { - e(Not.Defined("Language code"), nameof(command.Language)); - } - else if (languages.Contains(command.Language)) - { - e("Language has already been added."); - } - }); - } - - public static void CanRemove(LanguagesConfig languages, RemoveLanguage command) - { - Guard.NotNull(command, nameof(command)); - - var config = GetConfigOrThrow(languages, command.Language); - - Validate.It(() => "Cannot remove language.", e => - { - if (command.Language == null) - { - e(Not.Defined("Language code"), nameof(command.Language)); - } - - if (languages.Master == config) - { - e("Master language cannot be removed."); - } - }); - } - - public static void CanUpdate(LanguagesConfig languages, UpdateLanguage command) - { - Guard.NotNull(command, nameof(command)); - - var config = GetConfigOrThrow(languages, command.Language); - - Validate.It(() => "Cannot update language.", e => - { - if (command.Language == null) - { - e(Not.Defined("Language code"), nameof(command.Language)); - } - - if ((languages.Master == config || command.IsMaster) && command.IsOptional) - { - e("Master language cannot be made optional.", nameof(command.IsMaster)); - } - - if (command.Fallback == null) - { - return; - } - - foreach (var fallback in command.Fallback) - { - if (!languages.Contains(fallback)) - { - e($"App does not have fallback language '{fallback}'.", nameof(command.Fallback)); - } - } - }); - } - - private static LanguageConfig GetConfigOrThrow(LanguagesConfig languages, Language language) - { - if (language == null) - { - return null; - } - - if (!languages.TryGetConfig(language, out var languageConfig)) - { - throw new DomainObjectNotFoundException(language, "Languages", typeof(IAppEntity)); - } - - return languageConfig; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs deleted file mode 100644 index d5c0437d9..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== -using System; -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public static class GuardAppPatterns - { - public static void CanAdd(AppPatterns patterns, AddPattern command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot add pattern.", e => - { - if (command.PatternId == Guid.Empty) - { - e(Not.Defined("Id"), nameof(command.PatternId)); - } - - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined("Name"), nameof(command.Name)); - } - - if (patterns.Values.Any(x => x.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase))) - { - e("A pattern with the same name already exists."); - } - - if (string.IsNullOrWhiteSpace(command.Pattern)) - { - e(Not.Defined("Pattern"), nameof(command.Pattern)); - } - else if (!command.Pattern.IsValidRegex()) - { - e(Not.Valid("Pattern"), nameof(command.Pattern)); - } - - if (patterns.Values.Any(x => x.Pattern == command.Pattern)) - { - e("This pattern already exists but with another name."); - } - }); - } - - public static void CanDelete(AppPatterns patterns, DeletePattern command) - { - Guard.NotNull(command, nameof(command)); - - if (!patterns.ContainsKey(command.PatternId)) - { - throw new DomainObjectNotFoundException(command.PatternId.ToString(), typeof(AppPattern)); - } - } - - public static void CanUpdate(AppPatterns patterns, UpdatePattern command) - { - Guard.NotNull(command, nameof(command)); - - if (!patterns.ContainsKey(command.PatternId)) - { - throw new DomainObjectNotFoundException(command.PatternId.ToString(), typeof(AppPattern)); - } - - Validate.It(() => "Cannot update pattern.", e => - { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined("Name"), nameof(command.Name)); - } - - if (patterns.Any(x => x.Key != command.PatternId && x.Value.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase))) - { - e("A pattern with the same name already exists."); - } - - if (string.IsNullOrWhiteSpace(command.Pattern)) - { - e(Not.Defined("Pattern"), nameof(command.Pattern)); - } - else if (!command.Pattern.IsValidRegex()) - { - e(Not.Valid("Pattern"), nameof(command.Pattern)); - } - - if (patterns.Any(x => x.Key != command.PatternId && x.Value.Pattern == command.Pattern)) - { - e("This pattern already exists but with another name."); - } - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs deleted file mode 100644 index bd75bc92e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public static class GuardAppRoles - { - public static void CanAdd(Roles roles, AddRole command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot add role.", e => - { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined("Name"), nameof(command.Name)); - } - else if (roles.Contains(command.Name)) - { - e("A role with the same name already exists."); - } - }); - } - - public static void CanDelete(Roles roles, DeleteRole command, AppContributors contributors, AppClients clients) - { - Guard.NotNull(command, nameof(command)); - - CheckRoleExists(roles, command.Name); - - Validate.It(() => "Cannot delete role.", e => - { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined("Name"), nameof(command.Name)); - } - else if (Roles.IsDefault(command.Name)) - { - e("Cannot delete a default role."); - } - - if (clients.Values.Any(x => string.Equals(x.Role, command.Name, StringComparison.OrdinalIgnoreCase))) - { - e("Cannot remove a role when a client is assigned."); - } - - if (contributors.Values.Any(x => string.Equals(x, command.Name, StringComparison.OrdinalIgnoreCase))) - { - e("Cannot remove a role when a contributor is assigned."); - } - }); - } - - public static void CanUpdate(Roles roles, UpdateRole command) - { - Guard.NotNull(command, nameof(command)); - - CheckRoleExists(roles, command.Name); - - Validate.It(() => "Cannot delete role.", e => - { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined("Name"), nameof(command.Name)); - } - else if (Roles.IsDefault(command.Name)) - { - e("Cannot update a default role."); - } - - if (command.Permissions == null) - { - e(Not.Defined("Permissions"), nameof(command.Permissions)); - } - }); - } - - private static void CheckRoleExists(Roles roles, string name) - { - if (string.IsNullOrWhiteSpace(name) || Roles.IsDefault(name)) - { - return; - } - - if (!roles.ContainsCustom(name)) - { - throw new DomainObjectNotFoundException(name, "Roles", typeof(IAppEntity)); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs deleted file mode 100644 index 9ec0f9144..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs +++ /dev/null @@ -1,108 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public static class GuardAppWorkflows - { - public static void CanAdd(AddWorkflow command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot add workflow.", e => - { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined("Name"), nameof(command.Name)); - } - }); - } - - public static void CanUpdate(Workflows workflows, UpdateWorkflow command) - { - Guard.NotNull(command, nameof(command)); - - CheckWorkflowExists(workflows, command.WorkflowId); - - Validate.It(() => "Cannot update workflow.", e => - { - if (command.Workflow == null) - { - e(Not.Defined("Workflow"), nameof(command.Workflow)); - return; - } - - var workflow = command.Workflow; - - if (!workflow.Steps.ContainsKey(workflow.Initial)) - { - e(Not.Defined("Initial step"), $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}"); - } - - if (workflow.Initial == Status.Published) - { - e("Initial step cannot be published step.", $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}"); - } - - var stepsPrefix = $"{nameof(command.Workflow)}.{nameof(workflow.Steps)}"; - - if (!workflow.Steps.ContainsKey(Status.Published)) - { - e("Workflow must have a published step.", stepsPrefix); - } - - foreach (var step in workflow.Steps) - { - var stepPrefix = $"{stepsPrefix}.{step.Key}"; - - if (step.Value == null) - { - e(Not.Defined("Step"), stepPrefix); - } - else - { - foreach (var transition in step.Value.Transitions) - { - var transitionPrefix = $"{stepPrefix}.{nameof(step.Value.Transitions)}.{transition.Key}"; - - if (!workflow.Steps.ContainsKey(transition.Key)) - { - e("Transition has an invalid target.", transitionPrefix); - } - - if (transition.Value == null) - { - e(Not.Defined("Transition"), transitionPrefix); - } - } - } - } - }); - } - - public static void CanDelete(Workflows workflows, DeleteWorkflow command) - { - Guard.NotNull(command, nameof(command)); - - CheckWorkflowExists(workflows, command.WorkflowId); - } - - private static void CheckWorkflowExists(Workflows workflows, Guid id) - { - if (!workflows.ContainsKey(id)) - { - throw new DomainObjectNotFoundException(id.ToString(), "Workflows", typeof(IAppEntity)); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs b/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs deleted file mode 100644 index 4b959e6e4..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public interface IAppEntity : - IEntity, - IEntityWithCreatedBy, - IEntityWithLastModifiedBy, - IEntityWithVersion - { - string Name { get; } - - string Label { get; } - - string Description { get; } - - Roles Roles { get; } - - AppPlan Plan { get; } - - AppImage Image { get; } - - AppClients Clients { get; } - - AppPatterns Patterns { get; } - - AppContributors Contributors { get; } - - LanguagesConfig LanguagesConfig { get; } - - Workflows Workflows { get; } - - bool IsArchived { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs b/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs deleted file mode 100644 index d9e0f8d45..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public interface IAppUISettings - { - Task GetAsync(Guid appId, string userId); - - Task SetAsync(Guid appId, string userId, string path, IJsonValue value); - - Task SetAsync(Guid appId, string userId, JsonObject settings); - - Task RemoveAsync(Guid appId, string userId, string path); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs deleted file mode 100644 index 025f33ee6..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs +++ /dev/null @@ -1,286 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.Validation; -using Squidex.Shared; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public sealed class AppsIndex : IAppsIndex, ICommandMiddleware - { - private readonly IGrainFactory grainFactory; - - public AppsIndex(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public async Task RebuildByContributorsAsync(Guid appId, HashSet contributors) - { - foreach (var contributorId in contributors) - { - await Index(contributorId).AddAsync(appId); - } - } - - public Task RebuildByContributorsAsync(string contributorId, HashSet apps) - { - return Index(contributorId).RebuildAsync(apps); - } - - public Task RebuildAsync(Dictionary appsByName) - { - return Index().RebuildAsync(appsByName); - } - - public Task RemoveReservationAsync(string token) - { - return Index().RemoveReservationAsync(token); - } - - public Task> GetIdsAsync() - { - return Index().GetIdsAsync(); - } - - public Task AddAsync(string token) - { - return Index().AddAsync(token); - } - - public Task ReserveAsync(Guid id, string name) - { - return Index().ReserveAsync(id, name); - } - - public async Task> GetAppsAsync() - { - using (Profiler.TraceMethod()) - { - var ids = await GetAppIdsAsync(); - - var apps = - await Task.WhenAll(ids - .Select(id => GetAppAsync(id))); - - return apps.Where(x => x != null).ToList(); - } - } - - public async Task> GetAppsForUserAsync(string userId, PermissionSet permissions) - { - using (Profiler.TraceMethod()) - { - var ids = - await Task.WhenAll( - GetAppIdsByUserAsync(userId), - GetAppIdsAsync(permissions.ToAppNames())); - - var apps = - await Task.WhenAll(ids - .SelectMany(x => x) - .Select(id => GetAppAsync(id))); - - return apps.Where(x => x != null).ToList(); - } - } - - public async Task GetAppByNameAsync(string name) - { - using (Profiler.TraceMethod()) - { - var appId = await GetAppIdAsync(name); - - if (appId == default) - { - return null; - } - - return await GetAppAsync(appId); - } - } - - public async Task GetAppAsync(Guid appId) - { - using (Profiler.TraceMethod()) - { - var app = await grainFactory.GetGrain(appId).GetStateAsync(); - - if (IsFound(app.Value)) - { - return app.Value; - } - - return null; - } - } - - private async Task> GetAppIdsByUserAsync(string userId) - { - using (Profiler.TraceMethod()) - { - return await grainFactory.GetGrain(userId).GetIdsAsync(); - } - } - - private async Task> GetAppIdsAsync() - { - using (Profiler.TraceMethod()) - { - return await grainFactory.GetGrain(SingleGrain.Id).GetIdsAsync(); - } - } - - private async Task> GetAppIdsAsync(string[] names) - { - using (Profiler.TraceMethod()) - { - return await grainFactory.GetGrain(SingleGrain.Id).GetIdsAsync(names); - } - } - - private async Task GetAppIdAsync(string name) - { - using (Profiler.TraceMethod()) - { - return await grainFactory.GetGrain(SingleGrain.Id).GetIdAsync(name); - } - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (context.Command is CreateApp createApp) - { - var index = Index(); - - string token = await CheckAppAsync(index, createApp); - - try - { - await next(); - } - finally - { - if (token != null) - { - if (context.IsCompleted) - { - await index.AddAsync(token); - - if (createApp.Actor.IsSubject) - { - await Index(createApp.Actor.Identifier).AddAsync(createApp.AppId); - } - } - else - { - await index.RemoveReservationAsync(token); - } - } - } - } - else - { - await next(); - - if (context.IsCompleted) - { - switch (context.Command) - { - case AssignContributor assignContributor: - await AssignContributorAsync(assignContributor); - break; - - case RemoveContributor removeContributor: - await RemoveContributorAsync(removeContributor); - break; - - case ArchiveApp archiveApp: - await ArchiveAppAsync(archiveApp); - break; - } - } - } - } - - private async Task CheckAppAsync(IAppsByNameIndexGrain index, CreateApp command) - { - var name = command.Name; - - if (name.IsSlug()) - { - var token = await index.ReserveAsync(command.AppId, name); - - if (token == null) - { - var error = new ValidationError("An app with this already exists."); - - throw new ValidationException("Cannot create app.", error); - } - - return token; - } - - return null; - } - - private Task AssignContributorAsync(AssignContributor command) - { - return Index(command.ContributorId).AddAsync(command.AppId); - } - - private Task RemoveContributorAsync(RemoveContributor command) - { - return Index(command.ContributorId).RemoveAsync(command.AppId); - } - - private async Task ArchiveAppAsync(ArchiveApp command) - { - var appId = command.AppId; - - var app = await grainFactory.GetGrain(appId).GetStateAsync(); - - if (IsFound(app.Value)) - { - await Index().RemoveAsync(appId); - } - - foreach (var contributorId in app.Value.Contributors.Keys) - { - await Index(contributorId).RemoveAsync(appId); - } - } - - private static bool IsFound(IAppEntity app) - { - return app.Version > EtagVersion.Empty && !app.IsArchived; - } - - private IAppsByNameIndexGrain Index() - { - return grainFactory.GetGrain(SingleGrain.Id); - } - - private IAppsByUserIndexGrain Index(string id) - { - return grainFactory.GetGrain(id); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs deleted file mode 100644 index 17b9f9aeb..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure.Security; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public interface IAppsIndex - { - Task> GetIdsAsync(); - - Task> GetAppsAsync(); - - Task> GetAppsForUserAsync(string userId, PermissionSet permissions); - - Task GetAppByNameAsync(string name); - - Task GetAppAsync(Guid appId); - - Task ReserveAsync(Guid id, string name); - - Task AddAsync(string token); - - Task RemoveReservationAsync(string token); - - Task RebuildByContributorsAsync(string contributorId, HashSet apps); - - Task RebuildAsync(Dictionary apps); - - Task RebuildByContributorsAsync(Guid appId, HashSet contributors); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs deleted file mode 100644 index 360335f10..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ========================================================================== -// 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.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Apps.Invitation -{ - public sealed class InviteUserCommandMiddleware : ICommandMiddleware - { - private readonly IUserResolver userResolver; - - public InviteUserCommandMiddleware(IUserResolver userResolver) - { - Guard.NotNull(userResolver, nameof(userResolver)); - - this.userResolver = userResolver; - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (context.Command is AssignContributor assignContributor && ShouldInvite(assignContributor)) - { - var created = await userResolver.CreateUserIfNotExists(assignContributor.ContributorId, true); - - await next(); - - if (created && context.PlainResult is IAppEntity app) - { - context.Complete(new InvitedResult { App = app }); - } - } - else - { - await next(); - } - } - - private static bool ShouldInvite(AssignContributor assignContributor) - { - return assignContributor.Invite && assignContributor.ContributorId.IsEmail(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs b/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs deleted file mode 100644 index a5db89f6f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Shared; - -#pragma warning disable IDE0028 // Simplify collection initialization - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class RolePermissionsProvider - { - private readonly IAppProvider appProvider; - - public RolePermissionsProvider(IAppProvider appProvider) - { - Guard.NotNull(appProvider, nameof(appProvider)); - - this.appProvider = appProvider; - } - - public async Task> GetPermissionsAsync(IAppEntity app) - { - var schemaNames = await GetSchemaNamesAsync(app); - - var result = new List { Permission.Any }; - - foreach (var permission in Permissions.ForAppsNonSchema) - { - if (permission.Length > Permissions.App.Length + 1) - { - var trimmed = permission.Substring(Permissions.App.Length + 1); - - if (trimmed.Length > 0) - { - result.Add(trimmed); - } - } - } - - foreach (var permission in Permissions.ForAppsSchema) - { - var trimmed = permission.Substring(Permissions.App.Length + 1); - - foreach (var schema in schemaNames) - { - var replaced = trimmed.Replace("{name}", schema); - - result.Add(replaced); - } - } - - return result; - } - - private async Task> GetSchemaNamesAsync(IAppEntity app) - { - var schemas = await appProvider.GetSchemasAsync(app.Id); - - var schemaNames = new List(); - - schemaNames.Add(Permission.Any); - schemaNames.AddRange(schemas.Select(x => x.SchemaDef.Name)); - - return schemaNames; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs deleted file mode 100644 index 59d0feed4..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Entities.Apps.Services -{ - public interface IAppLimitsPlan - { - string Id { get; } - - string Name { get; } - - string Costs { get; } - - string YearlyCosts { get; } - - string YearlyId { get; } - - long MaxApiCalls { get; } - - long MaxAssetSize { get; } - - int MaxContributors { get; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs deleted file mode 100644 index 933a11ddf..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Apps.Services -{ - public interface IAppPlanBillingManager - { - bool HasPortal { get; } - - Task ChangePlanAsync(string userId, NamedId appId, string planId); - - Task GetPortalLinkAsync(string userId); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs deleted file mode 100644 index cd4fb8b03..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; - -namespace Squidex.Domain.Apps.Entities.Apps.Services -{ - public interface IAppPlansProvider - { - IEnumerable GetAvailablePlans(); - - bool IsConfiguredPlan(string planId); - - IAppLimitsPlan GetPlanUpgradeForApp(IAppEntity app); - - IAppLimitsPlan GetPlanUpgrade(string planId); - - IAppLimitsPlan GetPlanForApp(IAppEntity app); - - IAppLimitsPlan GetPlan(string planId); - - IAppLimitsPlan GetFreePlan(); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs deleted file mode 100644 index 3d568c928..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations -{ - public sealed class ConfigAppLimitsPlan : IAppLimitsPlan - { - public string Id { get; set; } - - public string Name { get; set; } - - public string Costs { get; set; } - - public string YearlyCosts { get; set; } - - public string YearlyId { get; set; } - - public long MaxApiCalls { get; set; } - - public long MaxAssetSize { get; set; } - - public int MaxContributors { get; set; } - - public ConfigAppLimitsPlan Clone() - { - return (ConfigAppLimitsPlan)MemberwiseClone(); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs deleted file mode 100644 index 3da2ae205..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations -{ - public sealed class ConfigAppPlansProvider : IAppPlansProvider - { - private static readonly ConfigAppLimitsPlan Infinite = new ConfigAppLimitsPlan - { - Id = "infinite", - Name = "Infinite", - MaxApiCalls = -1, - MaxAssetSize = -1, - MaxContributors = -1 - }; - - private readonly Dictionary plansById = new Dictionary(StringComparer.OrdinalIgnoreCase); - private readonly List plansList = new List(); - - public ConfigAppPlansProvider(IEnumerable config) - { - Guard.NotNull(config, nameof(config)); - - foreach (var plan in config.OrderBy(x => x.MaxApiCalls).Select(x => x.Clone())) - { - plansList.Add(plan); - plansById[plan.Id] = plan; - - if (!string.IsNullOrWhiteSpace(plan.YearlyId) && !string.IsNullOrWhiteSpace(plan.YearlyCosts)) - { - plansById[plan.YearlyId] = plan; - } - } - } - - public IEnumerable GetAvailablePlans() - { - return plansList; - } - - public bool IsConfiguredPlan(string planId) - { - return planId != null && plansById.ContainsKey(planId); - } - - public IAppLimitsPlan GetPlanForApp(IAppEntity app) - { - Guard.NotNull(app, nameof(app)); - - return GetPlan(app.Plan?.PlanId); - } - - public IAppLimitsPlan GetPlan(string planId) - { - return GetPlanCore(planId); - } - - public IAppLimitsPlan GetFreePlan() - { - return GetPlanCore(plansList.FirstOrDefault(x => string.IsNullOrWhiteSpace(x.Costs))?.Id); - } - - public IAppLimitsPlan GetPlanUpgradeForApp(IAppEntity app) - { - Guard.NotNull(app, nameof(app)); - - return GetPlanUpgrade(app.Plan?.PlanId); - } - - public IAppLimitsPlan GetPlanUpgrade(string planId) - { - var plan = GetPlanCore(planId); - - var nextPlanIndex = plansList.IndexOf(plan); - - if (nextPlanIndex >= 0 && nextPlanIndex < plansList.Count - 1) - { - return plansList[nextPlanIndex + 1]; - } - - return null; - } - - private ConfigAppLimitsPlan GetPlanCore(string planId) - { - return plansById.GetOrDefault(planId ?? string.Empty) ?? plansById.Values.FirstOrDefault() ?? Infinite; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs deleted file mode 100644 index b8c1f46ef..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations -{ - public sealed class NoopAppPlanBillingManager : IAppPlanBillingManager - { - public bool HasPortal - { - get { return false; } - } - - public Task ChangePlanAsync(string userId, NamedId appId, string planId) - { - return Task.FromResult(new PlanResetResult()); - } - - public Task GetPortalLinkAsync(string userId) - { - return Task.FromResult(string.Empty); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs deleted file mode 100644 index 9b41c88e2..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Apps.Services -{ - public sealed class RedirectToCheckoutResult : IChangePlanResult - { - public Uri Url { get; } - - public RedirectToCheckoutResult(Uri url) - { - Guard.NotNull(url, nameof(url)); - - Url = url; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs deleted file mode 100644 index 5247bb3dc..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ /dev/null @@ -1,252 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Runtime.Serialization; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -#pragma warning disable IDE0060 // Remove unused parameter - -namespace Squidex.Domain.Apps.Entities.Apps.State -{ - [CollectionName("Apps")] - public class AppState : DomainObjectState, IAppEntity - { - [DataMember] - public string Name { get; set; } - - [DataMember] - public string Label { get; set; } - - [DataMember] - public string Description { get; set; } - - [DataMember] - public Roles Roles { get; set; } = Roles.Empty; - - [DataMember] - public AppPlan Plan { get; set; } - - [DataMember] - public AppImage Image { get; set; } - - [DataMember] - public AppClients Clients { get; set; } = AppClients.Empty; - - [DataMember] - public AppPatterns Patterns { get; set; } = AppPatterns.Empty; - - [DataMember] - public AppContributors Contributors { get; set; } = AppContributors.Empty; - - [DataMember] - public LanguagesConfig LanguagesConfig { get; set; } = LanguagesConfig.English; - - [DataMember] - public Workflows Workflows { get; set; } = Workflows.Empty; - - [DataMember] - public bool IsArchived { get; set; } - - public void ApplyEvent(IEvent @event) - { - switch (@event) - { - case AppCreated e: - { - SimpleMapper.Map(e, this); - - break; - } - - case AppUpdated e: - { - SimpleMapper.Map(e, this); - - break; - } - - case AppImageUploaded e: - { - Image = e.Image; - - break; - } - - case AppImageRemoved _: - { - Image = null; - - break; - } - - case AppPlanChanged e: - { - Plan = AppPlan.Build(e.Actor, e.PlanId); - - break; - } - - case AppPlanReset _: - { - Plan = null; - - break; - } - - case AppContributorAssigned e: - { - Contributors = Contributors.Assign(e.ContributorId, e.Role); - - break; - } - - case AppContributorRemoved e: - { - Contributors = Contributors.Remove(e.ContributorId); - - break; - } - - case AppClientAttached e: - { - Clients = Clients.Add(e.Id, e.Secret); - - break; - } - - case AppClientUpdated e: - { - Clients = Clients.Update(e.Id, e.Role); - - break; - } - - case AppClientRenamed e: - { - Clients = Clients.Rename(e.Id, e.Name); - - break; - } - - case AppClientRevoked e: - { - Clients = Clients.Revoke(e.Id); - - break; - } - - case AppWorkflowAdded e: - { - Workflows = Workflows.Add(e.WorkflowId, e.Name); - - break; - } - - case AppWorkflowUpdated e: - { - Workflows = Workflows.Update(e.WorkflowId, e.Workflow); - - break; - } - - case AppWorkflowDeleted e: - { - Workflows = Workflows.Remove(e.WorkflowId); - - break; - } - - case AppPatternAdded e: - { - Patterns = Patterns.Add(e.PatternId, e.Name, e.Pattern, e.Message); - - break; - } - - case AppPatternDeleted e: - { - Patterns = Patterns.Remove(e.PatternId); - - break; - } - - case AppPatternUpdated e: - { - Patterns = Patterns.Update(e.PatternId, e.Name, e.Pattern, e.Message); - - break; - } - - case AppRoleAdded e: - { - Roles = Roles.Add(e.Name); - - break; - } - - case AppRoleDeleted e: - { - Roles = Roles.Remove(e.Name); - - break; - } - - case AppRoleUpdated e: - { - Roles = Roles.Update(e.Name, e.Permissions); - - break; - } - - case AppLanguageAdded e: - { - LanguagesConfig = LanguagesConfig.Set(e.Language); - - break; - } - - case AppLanguageRemoved e: - { - LanguagesConfig = LanguagesConfig.Remove(e.Language); - - break; - } - - case AppLanguageUpdated e: - { - LanguagesConfig = LanguagesConfig.Set(e.Language, e.IsOptional, e.Fallback); - - if (e.IsMaster) - { - LanguagesConfig = LanguagesConfig.MakeMaster(e.Language); - } - - break; - } - - case AppArchived _: - { - Plan = null; - - IsArchived = true; - - break; - } - } - } - - public override AppState Apply(Envelope @event) - { - return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs b/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs deleted file mode 100644 index 29540e8a5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; - -namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders -{ - public abstract class FieldBuilder - { - private readonly UpsertSchemaField field; - - protected T Properties() where T : FieldProperties - { - return field.Properties as T; - } - - protected FieldBuilder(UpsertSchemaField field) - { - this.field = field; - } - - public FieldBuilder Label(string label) - { - field.Properties.Label = label; - - return this; - } - - public FieldBuilder Hints(string hints) - { - field.Properties.Hints = hints; - - return this; - } - - public FieldBuilder Localizable() - { - field.Partitioning = Partitioning.Language.Key; - - return this; - } - - public FieldBuilder Disabled() - { - field.IsDisabled = true; - - return this; - } - - public FieldBuilder Required() - { - field.Properties.IsRequired = true; - - return this; - } - - public FieldBuilder ShowInList() - { - field.Properties.IsListField = true; - - return this; - } - - public FieldBuilder ShowInReferences() - { - field.Properties.IsReferenceField = true; - - return this; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs b/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs deleted file mode 100644 index bc588ebb5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs +++ /dev/null @@ -1,149 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders -{ - public sealed class SchemaBuilder - { - private readonly CreateSchema command; - - public SchemaBuilder(CreateSchema command) - { - this.command = command; - } - - public static SchemaBuilder Create(string name) - { - var schemaName = name.ToKebabCase(); - - return new SchemaBuilder(new CreateSchema - { - Name = schemaName - }).Published().WithLabel(name); - } - - public SchemaBuilder WithLabel(string label) - { - command.Properties = command.Properties ?? new SchemaProperties(); - command.Properties.Label = label; - - return this; - } - - public SchemaBuilder WithScripts(SchemaScripts scripts) - { - command.Scripts = scripts; - - return this; - } - - public SchemaBuilder Published() - { - command.IsPublished = true; - - return this; - } - - public SchemaBuilder Singleton() - { - command.IsSingleton = true; - - return this; - } - - public SchemaBuilder AddAssets(string name, Action configure) - { - var field = AddField(name); - - configure(new AssetFieldBuilder(field)); - - return this; - } - - public SchemaBuilder AddBoolean(string name, Action configure) - { - var field = AddField(name); - - configure(new BooleanFieldBuilder(field)); - - return this; - } - - public SchemaBuilder AddDateTime(string name, Action configure) - { - var field = AddField(name); - - configure(new DateTimeFieldBuilder(field)); - - return this; - } - - public SchemaBuilder AddJson(string name, Action configure) - { - var field = AddField(name); - - configure(new JsonFieldBuilder(field)); - - return this; - } - - public SchemaBuilder AddNumber(string name, Action configure) - { - var field = AddField(name); - - configure(new NumberFieldBuilder(field)); - - return this; - } - - public SchemaBuilder AddString(string name, Action configure) - { - var field = AddField(name); - - configure(new StringFieldBuilder(field)); - - return this; - } - - public SchemaBuilder AddTags(string name, Action configure) - { - var field = AddField(name); - - configure(new TagsFieldBuilder(field)); - - return this; - } - - private UpsertSchemaField AddField(string name) where T : FieldProperties, new() - { - var field = new UpsertSchemaField - { - Name = name.ToCamelCase(), - Properties = new T - { - Label = name - } - }; - - command.Fields = command.Fields ?? new List(); - command.Fields.Add(field); - - return field; - } - - public CreateSchema Build() - { - return command; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs b/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs deleted file mode 100644 index 8cd60657d..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure.Collections; - -namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders -{ - public class StringFieldBuilder : FieldBuilder - { - public StringFieldBuilder(UpsertSchemaField field) - : base(field) - { - } - - public StringFieldBuilder AsTextArea() - { - Properties().Editor = StringFieldEditor.TextArea; - - return this; - } - - public StringFieldBuilder AsRichText() - { - Properties().Editor = StringFieldEditor.RichText; - - return this; - } - - public StringFieldBuilder AsDropDown(params string[] values) - { - Properties().AllowedValues = ReadOnlyCollection.Create(values); - Properties().Editor = StringFieldEditor.Dropdown; - - return this; - } - - public StringFieldBuilder Pattern(string pattern, string message = null) - { - Properties().Pattern = pattern; - Properties().PatternMessage = message; - - return this; - } - - public StringFieldBuilder Length(int maxLength, int minLength = 0) - { - Properties().MaxLength = maxLength; - Properties().MinLength = minLength; - - return this; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs deleted file mode 100644 index fd8fdb53e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public sealed class AssetChangedTriggerHandler : RuleTriggerHandler - { - private readonly IScriptEngine scriptEngine; - private readonly IAssetLoader assetLoader; - - public AssetChangedTriggerHandler(IScriptEngine scriptEngine, IAssetLoader assetLoader) - { - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(assetLoader, nameof(assetLoader)); - - this.scriptEngine = scriptEngine; - - this.assetLoader = assetLoader; - } - - protected override async Task CreateEnrichedEventAsync(Envelope @event) - { - var result = new EnrichedAssetEvent(); - - var asset = await assetLoader.GetAsync(@event.Payload.AssetId, @event.Headers.EventStreamNumber()); - - SimpleMapper.Map(asset, result); - - switch (@event.Payload) - { - case AssetCreated _: - result.Type = EnrichedAssetEventType.Created; - break; - case AssetAnnotated _: - result.Type = EnrichedAssetEventType.Annotated; - break; - case AssetUpdated _: - result.Type = EnrichedAssetEventType.Updated; - break; - case AssetDeleted _: - result.Type = EnrichedAssetEventType.Deleted; - break; - } - - result.Name = $"Asset{result.Type}"; - - return result; - } - - protected override bool Trigger(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger) - { - return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs deleted file mode 100644 index 9b4b3486b..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ /dev/null @@ -1,175 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Security.Cryptography; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Domain.Apps.Entities.Tags; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public sealed class AssetCommandMiddleware : GrainCommandMiddleware - { - private readonly IAssetStore assetStore; - private readonly IAssetEnricher assetEnricher; - private readonly IAssetQueryService assetQuery; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - private readonly IContextProvider contextProvider; - private readonly IEnumerable> tagGenerators; - - public AssetCommandMiddleware( - IGrainFactory grainFactory, - IAssetEnricher assetEnricher, - IAssetQueryService assetQuery, - IAssetStore assetStore, - IAssetThumbnailGenerator assetThumbnailGenerator, - IContextProvider contextProvider, - IEnumerable> tagGenerators) - : base(grainFactory) - { - Guard.NotNull(assetEnricher, nameof(assetEnricher)); - Guard.NotNull(assetStore, nameof(assetStore)); - Guard.NotNull(assetQuery, nameof(assetQuery)); - Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); - Guard.NotNull(contextProvider, nameof(contextProvider)); - Guard.NotNull(tagGenerators, nameof(tagGenerators)); - - this.assetStore = assetStore; - this.assetEnricher = assetEnricher; - this.assetQuery = assetQuery; - this.assetThumbnailGenerator = assetThumbnailGenerator; - this.contextProvider = contextProvider; - this.tagGenerators = tagGenerators; - } - - public override async Task HandleAsync(CommandContext context, Func next) - { - var tempFile = context.ContextId.ToString(); - - switch (context.Command) - { - case CreateAsset createAsset: - { - await EnrichWithImageInfosAsync(createAsset); - await EnrichWithHashAndUploadAsync(createAsset, tempFile); - - try - { - var ctx = contextProvider.Context.Clone().WithNoAssetEnrichment(); - - var existings = await assetQuery.QueryByHashAsync(ctx, createAsset.AppId.Id, createAsset.FileHash); - - foreach (var existing in existings) - { - if (IsDuplicate(existing, createAsset.File)) - { - var result = new AssetCreatedResult(existing, true); - - context.Complete(result); - - await next(); - return; - } - } - - GenerateTags(createAsset); - - await HandleCoreAsync(context, next); - - var asset = context.Result(); - - context.Complete(new AssetCreatedResult(asset, false)); - - await assetStore.CopyAsync(tempFile, createAsset.AssetId.ToString(), asset.FileVersion, null); - } - finally - { - await assetStore.DeleteAsync(tempFile); - } - - break; - } - - case UpdateAsset updateAsset: - { - await EnrichWithImageInfosAsync(updateAsset); - await EnrichWithHashAndUploadAsync(updateAsset, tempFile); - - try - { - await HandleCoreAsync(context, next); - - var asset = context.Result(); - - await assetStore.CopyAsync(tempFile, updateAsset.AssetId.ToString(), asset.FileVersion, null); - } - finally - { - await assetStore.DeleteAsync(tempFile); - } - - break; - } - - default: - await HandleCoreAsync(context, next); - break; - } - } - - private async Task HandleCoreAsync(CommandContext context, Func next) - { - await base.HandleAsync(context, next); - - if (context.PlainResult is IAssetEntity asset && !(context.PlainResult is IEnrichedAssetEntity)) - { - var enriched = await assetEnricher.EnrichAsync(asset, contextProvider.Context); - - context.Complete(enriched); - } - } - - private static bool IsDuplicate(IAssetEntity asset, AssetFile file) - { - return asset?.FileName == file.FileName && asset.FileSize == file.FileSize; - } - - private async Task EnrichWithImageInfosAsync(UploadAssetCommand command) - { - command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); - } - - private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile) - { - using (var hashStream = new HasherStream(command.File.OpenRead(), HashAlgorithmName.SHA256)) - { - await assetStore.UploadAsync(tempFile, hashStream); - - command.FileHash = $"{hashStream.GetHashStringAndReset()}{command.File.FileName}{command.File.FileSize}".Sha256Base64(); - } - } - - private void GenerateTags(CreateAsset createAsset) - { - if (createAsset.Tags == null) - { - createAsset.Tags = new HashSet(); - } - - foreach (var tagGenerator in tagGenerators) - { - tagGenerator.GenerateTags(createAsset, createAsset.Tags); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs deleted file mode 100644 index 23c80b139..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs +++ /dev/null @@ -1,183 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Domain.Apps.Entities.Assets.Guards; -using Squidex.Domain.Apps.Entities.Assets.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public sealed class AssetGrain : LogSnapshotDomainObjectGrain, IAssetGrain - { - private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); - private readonly ITagService tagService; - - public AssetGrain(IStore store, ITagService tagService, IActivationLimit limit, ISemanticLog log) - : base(store, log) - { - Guard.NotNull(tagService, nameof(tagService)); - - this.tagService = tagService; - - limit?.SetLimit(5000, Lifetime); - } - - protected override Task OnActivateAsync(Guid key) - { - TryDelayDeactivation(Lifetime); - - return base.OnActivateAsync(key); - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - VerifyNotDeleted(); - - switch (command) - { - case CreateAsset createAsset: - return CreateReturnAsync(createAsset, async c => - { - GuardAsset.CanCreate(c); - - var tagIds = await NormalizeTagsAsync(c.AppId.Id, c.Tags); - - Create(c, tagIds); - - return Snapshot; - }); - case UpdateAsset updateAsset: - return UpdateReturn(updateAsset, c => - { - GuardAsset.CanUpdate(c); - - Update(c); - - return Snapshot; - }); - case AnnotateAsset annotateAsset: - return UpdateReturnAsync(annotateAsset, async c => - { - GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug); - - var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags); - - Annotate(c, tagIds); - - return Snapshot; - }); - case DeleteAsset deleteAsset: - return UpdateAsync(deleteAsset, async c => - { - GuardAsset.CanDelete(c); - - await tagService.NormalizeTagsAsync(Snapshot.AppId.Id, TagGroups.Assets, null, Snapshot.Tags); - - Delete(c); - }); - default: - throw new NotSupportedException(); - } - } - - private async Task> NormalizeTagsAsync(Guid appId, HashSet tags) - { - if (tags == null) - { - return null; - } - - var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Assets, tags, Snapshot.Tags); - - return new HashSet(normalized.Values); - } - - public void Create(CreateAsset command, HashSet tagIds) - { - var @event = SimpleMapper.Map(command, new AssetCreated - { - IsImage = command.ImageInfo != null, - FileName = command.File.FileName, - FileSize = command.File.FileSize, - FileVersion = 0, - MimeType = command.File.MimeType, - PixelWidth = command.ImageInfo?.PixelWidth, - PixelHeight = command.ImageInfo?.PixelHeight, - Slug = command.File.FileName.ToAssetSlug() - }); - - @event.Tags = tagIds; - - RaiseEvent(@event); - } - - public void Update(UpdateAsset command) - { - var @event = SimpleMapper.Map(command, new AssetUpdated - { - FileVersion = Snapshot.FileVersion + 1, - FileSize = command.File.FileSize, - MimeType = command.File.MimeType, - PixelWidth = command.ImageInfo?.PixelWidth, - PixelHeight = command.ImageInfo?.PixelHeight, - IsImage = command.ImageInfo != null - }); - - RaiseEvent(@event); - } - - public void Annotate(AnnotateAsset command, HashSet tagIds) - { - var @event = SimpleMapper.Map(command, new AssetAnnotated()); - - @event.Tags = tagIds; - - RaiseEvent(@event); - } - - public void Delete(DeleteAsset command) - { - RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize })); - } - - private void RaiseEvent(AppEvent @event) - { - if (@event.AppId == null) - { - @event.AppId = Snapshot.AppId; - } - - RaiseEvent(Envelope.Create(@event)); - } - - private void VerifyNotDeleted() - { - if (Snapshot.IsDeleted) - { - throw new DomainException("Asset has already been deleted"); - } - } - - public Task> GetStateAsync(long version = EtagVersion.Any) - { - return J.AsTask(GetSnapshot(version)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs deleted file mode 100644 index eb6eb6168..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.UsageTracking; - -#pragma warning disable CS0649 - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public partial class AssetUsageTracker : IAssetUsageTracker, IEventConsumer - { - private const string Category = "Default"; - private const string CounterTotalCount = "TotalAssets"; - private const string CounterTotalSize = "TotalSize"; - private static readonly DateTime SummaryDate; - private readonly IUsageRepository usageStore; - - public AssetUsageTracker(IUsageRepository usageStore) - { - Guard.NotNull(usageStore, nameof(usageStore)); - - this.usageStore = usageStore; - } - - public async Task GetTotalSizeAsync(Guid appId) - { - var key = GetKey(appId); - - var entries = await usageStore.QueryAsync(key, SummaryDate, SummaryDate); - - return (long)entries.Select(x => x.Counters.Get(CounterTotalSize)).FirstOrDefault(); - } - - public async Task> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate) - { - var enriched = new List(); - - var usagesFlat = await usageStore.QueryAsync(GetKey(appId), fromDate, toDate); - - for (var date = fromDate; date <= toDate; date = date.AddDays(1)) - { - var stored = usagesFlat.FirstOrDefault(x => x.Date == date && x.Category == Category); - - var totalCount = 0L; - var totalSize = 0L; - - if (stored != null) - { - totalCount = (long)stored.Counters.Get(CounterTotalCount); - totalSize = (long)stored.Counters.Get(CounterTotalSize); - } - - enriched.Add(new AssetStats(date, totalCount, totalSize)); - } - - return enriched; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs deleted file mode 100644 index 44701ee16..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs +++ /dev/null @@ -1,127 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Domain.Apps.Entities.Assets.State; -using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public sealed class BackupAssets : BackupHandlerWithStore - { - private const string TagsFile = "AssetTags.json"; - private readonly HashSet assetIds = new HashSet(); - private readonly IAssetStore assetStore; - private readonly ITagService tagService; - - public override string Name { get; } = "Assets"; - - public BackupAssets(IStore store, IAssetStore assetStore, ITagService tagService) - : base(store) - { - Guard.NotNull(assetStore, nameof(assetStore)); - Guard.NotNull(tagService, nameof(tagService)); - - this.assetStore = assetStore; - - this.tagService = tagService; - } - - public override Task BackupAsync(Guid appId, BackupWriter writer) - { - return BackupTagsAsync(appId, writer); - } - - public override Task BackupEventAsync(Envelope @event, Guid appId, BackupWriter writer) - { - switch (@event.Payload) - { - case AssetCreated assetCreated: - return WriteAssetAsync(assetCreated.AssetId, assetCreated.FileVersion, writer); - case AssetUpdated assetUpdated: - return WriteAssetAsync(assetUpdated.AssetId, assetUpdated.FileVersion, writer); - } - - return TaskHelper.Done; - } - - public override async Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) - { - switch (@event.Payload) - { - case AssetCreated assetCreated: - await ReadAssetAsync(assetCreated.AssetId, assetCreated.FileVersion, reader); - break; - case AssetUpdated assetUpdated: - await ReadAssetAsync(assetUpdated.AssetId, assetUpdated.FileVersion, reader); - break; - } - - return true; - } - - public override async Task RestoreAsync(Guid appId, BackupReader reader) - { - await RestoreTagsAsync(appId, reader); - - await RebuildManyAsync(assetIds, RebuildAsync); - } - - private async Task RestoreTagsAsync(Guid appId, BackupReader reader) - { - var tags = await reader.ReadJsonAttachmentAsync(TagsFile); - - await tagService.RebuildTagsAsync(appId, TagGroups.Assets, tags); - } - - private async Task BackupTagsAsync(Guid appId, BackupWriter writer) - { - var tags = await tagService.GetExportableTagsAsync(appId, TagGroups.Assets); - - await writer.WriteJsonAsync(TagsFile, tags); - } - - private Task WriteAssetAsync(Guid assetId, long fileVersion, BackupWriter writer) - { - return writer.WriteBlobAsync(GetName(assetId, fileVersion), stream => - { - return assetStore.DownloadAsync(assetId.ToString(), fileVersion, null, stream); - }); - } - - private Task ReadAssetAsync(Guid assetId, long fileVersion, BackupReader reader) - { - assetIds.Add(assetId); - - return reader.ReadBlobAsync(GetName(reader.OldGuid(assetId), fileVersion), async stream => - { - try - { - await assetStore.UploadAsync(assetId.ToString(), fileVersion, null, stream, true); - } - catch (AssetAlreadyExistsException) - { - return; - } - }); - } - - private static string GetName(Guid assetId, long fileVersion) - { - return $"{assetId}_{fileVersion}.asset"; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs deleted file mode 100644 index 5ef0652cd..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure.Assets; - -namespace Squidex.Domain.Apps.Entities.Assets.Commands -{ - public abstract class UploadAssetCommand : AssetCommand - { - public AssetFile File { get; set; } - - public ImageInfo ImageInfo { get; set; } - - public string FileHash { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs deleted file mode 100644 index 06746132f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Assets.Guards -{ - public static class GuardAsset - { - public static void CanAnnotate(AnnotateAsset command, string oldFileName, string oldSlug) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot rename asset.", e => - { - if (string.IsNullOrWhiteSpace(command.FileName) && - string.IsNullOrWhiteSpace(command.Slug) && - command.Tags == null) - { - e("Either file name, slug or tags must be defined.", nameof(command.FileName), nameof(command.Slug), nameof(command.Tags)); - } - - if (!string.IsNullOrWhiteSpace(command.FileName) && string.Equals(command.FileName, oldFileName)) - { - e(Not.New("Asset", "name"), nameof(command.FileName)); - } - - if (!string.IsNullOrWhiteSpace(command.Slug) && string.Equals(command.Slug, oldSlug)) - { - e(Not.New("Asset", "slug"), nameof(command.Slug)); - } - }); - } - - public static void CanCreate(CreateAsset command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanUpdate(UpdateAsset command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanDelete(DeleteAsset command) - { - Guard.NotNull(command, nameof(command)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs deleted file mode 100644 index 395a4d86f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public interface IAssetQueryService - { - Task> QueryByHashAsync(Context context, Guid appId, string hash); - - Task> QueryAsync(Context context, Q query); - - Task FindAssetAsync(Context context, Guid id); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs b/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs deleted file mode 100644 index 3c79c93d5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Assets.Queries -{ - public sealed class AssetEnricher : IAssetEnricher - { - private readonly ITagService tagService; - - public AssetEnricher(ITagService tagService) - { - Guard.NotNull(tagService, nameof(tagService)); - - this.tagService = tagService; - } - - public async Task EnrichAsync(IAssetEntity asset, Context context) - { - Guard.NotNull(asset, nameof(asset)); - Guard.NotNull(context, nameof(context)); - - var enriched = await EnrichAsync(Enumerable.Repeat(asset, 1), context); - - return enriched[0]; - } - - public async Task> EnrichAsync(IEnumerable assets, Context context) - { - Guard.NotNull(assets, nameof(assets)); - Guard.NotNull(context, nameof(context)); - - using (Profiler.TraceMethod()) - { - var results = assets.Select(x => SimpleMapper.Map(x, new AssetEntity())).ToList(); - - if (ShouldEnrich(context)) - { - await EnrichTagsAsync(results); - } - - return results; - } - } - - private async Task EnrichTagsAsync(List assets) - { - foreach (var group in assets.GroupBy(x => x.AppId.Id)) - { - var tagsById = await CalculateTags(group); - - foreach (var asset in group) - { - asset.TagNames = new HashSet(); - - if (asset.Tags != null) - { - foreach (var id in asset.Tags) - { - if (tagsById.TryGetValue(id, out var name)) - { - asset.TagNames.Add(name); - } - } - } - } - } - } - - private async Task> CalculateTags(IGrouping group) - { - var uniqueIds = group.Where(x => x.Tags != null).SelectMany(x => x.Tags).ToHashSet(); - - return await tagService.DenormalizeTagsAsync(group.Key, TagGroups.Assets, uniqueIds); - } - - private static bool ShouldEnrich(Context context) - { - return !context.IsNoAssetEnrichment(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs b/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs deleted file mode 100644 index 82adbe7db..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Domain.Apps.Entities.Assets.Queries -{ - public sealed class AssetLoader : IAssetLoader - { - private readonly IGrainFactory grainFactory; - - public AssetLoader(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public async Task GetAsync(Guid id, long version) - { - using (Profiler.TraceMethod()) - { - var grain = grainFactory.GetGrain(id); - - var content = await grain.GetStateAsync(version); - - if (content.Value == null || content.Value.Version != version) - { - throw new DomainObjectNotFoundException(id.ToString(), typeof(IAssetEntity)); - } - - return content.Value; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs b/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs deleted file mode 100644 index 66efc0a53..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs +++ /dev/null @@ -1,174 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Options; -using Microsoft.OData; -using Microsoft.OData.Edm; -using NJsonSchema; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.Queries.OData; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Assets.Queries -{ - public class AssetQueryParser - { - private readonly JsonSchema jsonSchema = BuildJsonSchema(); - private readonly IEdmModel edmModel = BuildEdmModel(); - private readonly IJsonSerializer jsonSerializer; - private readonly ITagService tagService; - private readonly AssetOptions options; - - public AssetQueryParser(IJsonSerializer jsonSerializer, ITagService tagService, IOptions options) - { - Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); - Guard.NotNull(options, nameof(options)); - Guard.NotNull(tagService, nameof(tagService)); - - this.jsonSerializer = jsonSerializer; - this.options = options.Value; - this.tagService = tagService; - } - - public virtual ClrQuery ParseQuery(Context context, Q q) - { - Guard.NotNull(context, nameof(context)); - - using (Profiler.TraceMethod()) - { - var result = new ClrQuery(); - - if (!string.IsNullOrWhiteSpace(q?.JsonQuery)) - { - result = ParseJson(q.JsonQuery); - } - else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) - { - result = ParseOData(q.ODataQuery); - } - - if (result.Filter != null) - { - result.Filter = FilterTagTransformer.Transform(result.Filter, context.App.Id, tagService); - } - - if (result.Sort.Count == 0) - { - result.Sort.Add(new SortNode(new List { "lastModified" }, SortOrder.Descending)); - } - - if (result.Take == long.MaxValue) - { - result.Take = options.DefaultPageSize; - } - else if (result.Take > options.MaxResults) - { - result.Take = options.MaxResults; - } - - return result; - } - } - - private ClrQuery ParseJson(string json) - { - return jsonSchema.Parse(json, jsonSerializer); - } - - private ClrQuery ParseOData(string odata) - { - try - { - return edmModel.ParseQuery(odata).ToQuery(); - } - catch (NotSupportedException) - { - throw new ValidationException("OData operation is not supported."); - } - catch (ODataException ex) - { - throw new ValidationException($"Failed to parse query: {ex.Message}", ex); - } - } - - private static JsonSchema BuildJsonSchema() - { - var schema = new JsonSchema { Title = "Asset", Type = JsonObjectType.Object }; - - void AddProperty(string name, JsonObjectType type, string format = null) - { - var property = new JsonSchemaProperty { Type = type, Format = format }; - - schema.Properties[name.ToCamelCase()] = property; - } - - AddProperty(nameof(IAssetEntity.Id), JsonObjectType.String, JsonFormatStrings.Guid); - AddProperty(nameof(IAssetEntity.Created), JsonObjectType.String, JsonFormatStrings.DateTime); - AddProperty(nameof(IAssetEntity.CreatedBy), JsonObjectType.String); - AddProperty(nameof(IAssetEntity.LastModified), JsonObjectType.String, JsonFormatStrings.DateTime); - AddProperty(nameof(IAssetEntity.LastModifiedBy), JsonObjectType.String); - AddProperty(nameof(IAssetEntity.Version), JsonObjectType.Integer); - AddProperty(nameof(IAssetEntity.FileName), JsonObjectType.String); - AddProperty(nameof(IAssetEntity.FileHash), JsonObjectType.String); - AddProperty(nameof(IAssetEntity.FileSize), JsonObjectType.Integer); - AddProperty(nameof(IAssetEntity.FileVersion), JsonObjectType.Integer); - AddProperty(nameof(IAssetEntity.IsImage), JsonObjectType.Boolean); - AddProperty(nameof(IAssetEntity.MimeType), JsonObjectType.String); - AddProperty(nameof(IAssetEntity.PixelHeight), JsonObjectType.Integer); - AddProperty(nameof(IAssetEntity.PixelWidth), JsonObjectType.Integer); - AddProperty(nameof(IAssetEntity.Slug), JsonObjectType.String); - AddProperty(nameof(IAssetEntity.Tags), JsonObjectType.String); - - return schema; - } - - private static IEdmModel BuildEdmModel() - { - var entityType = new EdmEntityType("Squidex", "Asset"); - - void AddProperty(string name, EdmPrimitiveTypeKind type) - { - entityType.AddStructuralProperty(name.ToCamelCase(), type); - } - - AddProperty(nameof(IAssetEntity.Id), EdmPrimitiveTypeKind.String); - AddProperty(nameof(IAssetEntity.Created), EdmPrimitiveTypeKind.DateTimeOffset); - AddProperty(nameof(IAssetEntity.CreatedBy), EdmPrimitiveTypeKind.String); - AddProperty(nameof(IAssetEntity.LastModified), EdmPrimitiveTypeKind.DateTimeOffset); - AddProperty(nameof(IAssetEntity.LastModifiedBy), EdmPrimitiveTypeKind.String); - AddProperty(nameof(IAssetEntity.Version), EdmPrimitiveTypeKind.Int64); - AddProperty(nameof(IAssetEntity.FileName), EdmPrimitiveTypeKind.String); - AddProperty(nameof(IAssetEntity.FileHash), EdmPrimitiveTypeKind.String); - AddProperty(nameof(IAssetEntity.FileSize), EdmPrimitiveTypeKind.Int64); - AddProperty(nameof(IAssetEntity.FileVersion), EdmPrimitiveTypeKind.Int64); - AddProperty(nameof(IAssetEntity.IsImage), EdmPrimitiveTypeKind.Boolean); - AddProperty(nameof(IAssetEntity.MimeType), EdmPrimitiveTypeKind.String); - AddProperty(nameof(IAssetEntity.PixelHeight), EdmPrimitiveTypeKind.Int32); - AddProperty(nameof(IAssetEntity.PixelWidth), EdmPrimitiveTypeKind.Int32); - AddProperty(nameof(IAssetEntity.Slug), EdmPrimitiveTypeKind.String); - AddProperty(nameof(IAssetEntity.Tags), EdmPrimitiveTypeKind.String); - - var container = new EdmEntityContainer("Squidex", "Container"); - - container.AddEntitySet("AssetSet", entityType); - - var model = new EdmModel(); - - model.AddElement(container); - model.AddElement(entityType); - - return model; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs deleted file mode 100644 index 376a057c1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs +++ /dev/null @@ -1,97 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Assets.Queries -{ - public sealed class AssetQueryService : IAssetQueryService - { - private readonly IAssetEnricher assetEnricher; - private readonly IAssetRepository assetRepository; - private readonly AssetQueryParser queryParser; - - public AssetQueryService( - IAssetEnricher assetEnricher, - IAssetRepository assetRepository, - AssetQueryParser queryParser) - { - Guard.NotNull(assetEnricher, nameof(assetEnricher)); - Guard.NotNull(assetRepository, nameof(assetRepository)); - Guard.NotNull(queryParser, nameof(queryParser)); - - this.assetEnricher = assetEnricher; - this.assetRepository = assetRepository; - this.queryParser = queryParser; - } - - public async Task FindAssetAsync(Context context, Guid id) - { - var asset = await assetRepository.FindAssetAsync(id); - - if (asset != null) - { - return await assetEnricher.EnrichAsync(asset, context); - } - - return null; - } - - public async Task> QueryByHashAsync(Context context, Guid appId, string hash) - { - Guard.NotNull(hash, nameof(hash)); - - var assets = await assetRepository.QueryByHashAsync(appId, hash); - - return await assetEnricher.EnrichAsync(assets, context); - } - - public async Task> QueryAsync(Context context, Q query) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(query, nameof(query)); - - IResultList assets; - - if (query.Ids != null && query.Ids.Count > 0) - { - assets = await QueryByIdsAsync(context, query); - } - else - { - assets = await QueryByQueryAsync(context, query); - } - - var enriched = await assetEnricher.EnrichAsync(assets, context); - - return ResultList.Create(assets.Total, enriched); - } - - private async Task> QueryByQueryAsync(Context context, Q query) - { - var parsedQuery = queryParser.ParseQuery(context, query); - - return await assetRepository.QueryAsync(context.App.Id, parsedQuery); - } - - private async Task> QueryByIdsAsync(Context context, Q query) - { - var assets = await assetRepository.QueryAsync(context.App.Id, new HashSet(query.Ids)); - - return Sort(assets, query.Ids); - } - - private static IResultList Sort(IResultList assets, IReadOnlyList ids) - { - return assets.SortSet(x => x.Id, ids); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs b/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs deleted file mode 100644 index af0f852b5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// 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.Tags; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.Assets.Queries -{ - public sealed class FilterTagTransformer : TransformVisitor - { - private readonly ITagService tagService; - private readonly Guid appId; - - private FilterTagTransformer(Guid appId, ITagService tagService) - { - this.appId = appId; - - this.tagService = tagService; - } - - public static FilterNode Transform(FilterNode nodeIn, Guid appId, ITagService tagService) - { - Guard.NotNull(nodeIn, nameof(nodeIn)); - Guard.NotNull(tagService, nameof(tagService)); - - return nodeIn.Accept(new FilterTagTransformer(appId, tagService)); - } - - public override FilterNode Visit(CompareFilter nodeIn) - { - if (string.Equals(nodeIn.Path[0], nameof(IAssetEntity.Tags), StringComparison.OrdinalIgnoreCase) && nodeIn.Value.Value is string stringValue) - { - var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, HashSet.Of(stringValue))).Result; - - if (tagNames.TryGetValue(stringValue, out var normalized)) - { - return new CompareFilter(nodeIn.Path, nodeIn.Operator, normalized); - } - } - - return nodeIn; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs deleted file mode 100644 index dde7fa42a..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.Assets.Repositories -{ - public interface IAssetRepository - { - Task> QueryByHashAsync(Guid appId, string hash); - - Task> QueryAsync(Guid appId, ClrQuery query); - - Task> QueryAsync(Guid appId, HashSet ids); - - Task FindAssetAsync(Guid id, bool allowDeleted = false); - - Task FindAssetBySlugAsync(Guid appId, string slug); - - Task RemoveAsync(Guid appId); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs deleted file mode 100644 index 7cf83f469..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs +++ /dev/null @@ -1,262 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using NodaTime; -using Orleans.Concurrency; -using Squidex.Domain.Apps.Entities.Backup.Helpers; -using Squidex.Domain.Apps.Entities.Backup.State; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - [Reentrant] - public sealed class BackupGrain : GrainOfGuid, IBackupGrain - { - private const int MaxBackups = 10; - private static readonly Duration UpdateDuration = Duration.FromSeconds(1); - private readonly IAssetStore assetStore; - private readonly IBackupArchiveLocation backupArchiveLocation; - private readonly IClock clock; - private readonly IJsonSerializer serializer; - private readonly IServiceProvider serviceProvider; - private readonly IEventDataFormatter eventDataFormatter; - private readonly IEventStore eventStore; - private readonly ISemanticLog log; - private readonly IGrainState state; - private CancellationTokenSource currentTask; - private BackupStateJob currentJob; - - public BackupGrain( - IAssetStore assetStore, - IBackupArchiveLocation backupArchiveLocation, - IClock clock, - IEventStore eventStore, - IEventDataFormatter eventDataFormatter, - IJsonSerializer serializer, - IServiceProvider serviceProvider, - ISemanticLog log, - IGrainState state) - { - Guard.NotNull(assetStore, nameof(assetStore)); - Guard.NotNull(backupArchiveLocation, nameof(backupArchiveLocation)); - Guard.NotNull(clock, nameof(clock)); - Guard.NotNull(eventStore, nameof(eventStore)); - Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); - Guard.NotNull(serviceProvider, nameof(serviceProvider)); - Guard.NotNull(serializer, nameof(serializer)); - Guard.NotNull(state, nameof(state)); - Guard.NotNull(log, nameof(log)); - - this.assetStore = assetStore; - this.backupArchiveLocation = backupArchiveLocation; - this.clock = clock; - this.eventStore = eventStore; - this.eventDataFormatter = eventDataFormatter; - this.serializer = serializer; - this.serviceProvider = serviceProvider; - this.state = state; - this.log = log; - } - - protected override Task OnActivateAsync(Guid key) - { - RecoverAfterRestartAsync().Forget(); - - return TaskHelper.Done; - } - - private async Task RecoverAfterRestartAsync() - { - foreach (var job in state.Value.Jobs) - { - if (!job.Stopped.HasValue) - { - var jobId = job.Id.ToString(); - - job.Stopped = clock.GetCurrentInstant(); - - await Safe.DeleteAsync(backupArchiveLocation, jobId, log); - await Safe.DeleteAsync(assetStore, jobId, log); - - job.Status = JobStatus.Failed; - - await state.WriteAsync(); - } - } - } - - public async Task RunAsync() - { - if (currentTask != null) - { - throw new DomainException("Another backup process is already running."); - } - - if (state.Value.Jobs.Count >= MaxBackups) - { - throw new DomainException($"You cannot have more than {MaxBackups} backups."); - } - - var job = new BackupStateJob - { - Id = Guid.NewGuid(), - Started = clock.GetCurrentInstant(), - Status = JobStatus.Started - }; - - currentTask = new CancellationTokenSource(); - currentJob = job; - - state.Value.Jobs.Insert(0, job); - - await state.WriteAsync(); - - Process(job, currentTask.Token); - } - - private void Process(BackupStateJob job, CancellationToken ct) - { - ProcessAsync(job, ct).Forget(); - } - - private async Task ProcessAsync(BackupStateJob job, CancellationToken ct) - { - var jobId = job.Id.ToString(); - - var handlers = CreateHandlers(); - - var lastTimestamp = job.Started; - - try - { - using (var stream = await backupArchiveLocation.OpenStreamAsync(jobId)) - { - using (var writer = new BackupWriter(serializer, stream, true)) - { - await eventStore.QueryAsync(async storedEvent => - { - var @event = eventDataFormatter.Parse(storedEvent.Data); - - writer.WriteEvent(storedEvent); - - foreach (var handler in handlers) - { - await handler.BackupEventAsync(@event, Key, writer); - } - - job.HandledEvents = writer.WrittenEvents; - job.HandledAssets = writer.WrittenAttachments; - - lastTimestamp = await WritePeriodically(lastTimestamp); - }, SquidexHeaders.AppId, Key.ToString(), null, ct); - - foreach (var handler in handlers) - { - await handler.BackupAsync(Key, writer); - } - - foreach (var handler in handlers) - { - await handler.CompleteBackupAsync(Key, writer); - } - } - - stream.Position = 0; - - ct.ThrowIfCancellationRequested(); - - await assetStore.UploadAsync(jobId, 0, null, stream, false, ct); - } - - job.Status = JobStatus.Completed; - } - catch (Exception ex) - { - log.LogError(ex, jobId, (ctx, w) => w - .WriteProperty("action", "makeBackup") - .WriteProperty("status", "failed") - .WriteProperty("backupId", ctx)); - - job.Status = JobStatus.Failed; - } - finally - { - await Safe.DeleteAsync(backupArchiveLocation, jobId, log); - - job.Stopped = clock.GetCurrentInstant(); - - await state.WriteAsync(); - - currentTask = null; - currentJob = null; - } - } - - private async Task WritePeriodically(Instant lastTimestamp) - { - var now = clock.GetCurrentInstant(); - - if ((now - lastTimestamp) >= UpdateDuration) - { - lastTimestamp = now; - - await state.WriteAsync(); - } - - return lastTimestamp; - } - - public async Task DeleteAsync(Guid id) - { - var job = state.Value.Jobs.FirstOrDefault(x => x.Id == id); - - if (job == null) - { - throw new DomainObjectNotFoundException(id.ToString(), typeof(IBackupJob)); - } - - if (currentJob == job) - { - currentTask?.Cancel(); - } - else - { - var jobId = job.Id.ToString(); - - await Safe.DeleteAsync(backupArchiveLocation, jobId, log); - await Safe.DeleteAsync(assetStore, jobId, log); - - state.Value.Jobs.Remove(job); - - await state.WriteAsync(); - } - } - - private IEnumerable CreateHandlers() - { - return serviceProvider.GetRequiredService>(); - } - - public Task>> GetStateAsync() - { - return J.AsTask(state.Value.Jobs.OfType().ToList()); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs deleted file mode 100644 index b717f017e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - public abstract class BackupHandlerWithStore : BackupHandler - { - private readonly IStore store; - - protected BackupHandlerWithStore(IStore store) - { - Guard.NotNull(store, nameof(store)); - - this.store = store; - } - - protected async Task RebuildManyAsync(IEnumerable ids, Func action) - { - foreach (var id in ids) - { - await action(id); - } - } - - protected async Task RebuildAsync(Guid key) where TState : IDomainState, new() - { - var state = new TState - { - Version = EtagVersion.Empty - }; - - var persistence = store.WithSnapshotsAndEventSourcing(typeof(TGrain), key, (TState s) => state = s, e => - { - state = state.Apply(e); - - state.Version++; - }); - - await persistence.ReadAsync(); - await persistence.WriteSnapshotAsync(state); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs deleted file mode 100644 index 3aa5bc9c5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs +++ /dev/null @@ -1,155 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Backup.Helpers; -using Squidex.Domain.Apps.Entities.Backup.Model; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.States; - -#pragma warning disable SA1401 // Fields must be private - -namespace Squidex.Domain.Apps.Entities.Backup -{ - public sealed class BackupReader : DisposableObjectBase - { - private readonly GuidMapper guidMapper = new GuidMapper(); - private readonly ZipArchive archive; - private readonly IJsonSerializer serializer; - private int readEvents; - private int readAttachments; - - public int ReadEvents - { - get { return readEvents; } - } - - public int ReadAttachments - { - get { return readAttachments; } - } - - public BackupReader(IJsonSerializer serializer, Stream stream) - { - Guard.NotNull(serializer, nameof(serializer)); - - this.serializer = serializer; - - archive = new ZipArchive(stream, ZipArchiveMode.Read, false); - } - - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - archive.Dispose(); - } - } - - public Guid OldGuid(Guid newId) - { - return guidMapper.OldGuid(newId); - } - - public Task ReadJsonAttachmentAsync(string name) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); - - if (attachmentEntry == null) - { - throw new FileNotFoundException("Cannot find attachment.", name); - } - - T result; - - using (var stream = attachmentEntry.Open()) - { - result = serializer.Deserialize(stream, null, guidMapper.NewGuidOrValue); - } - - readAttachments++; - - return Task.FromResult(result); - } - - public async Task ReadBlobAsync(string name, Func handler) - { - Guard.NotNullOrEmpty(name, nameof(name)); - Guard.NotNull(handler, nameof(handler)); - - var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); - - if (attachmentEntry == null) - { - throw new FileNotFoundException("Cannot find attachment.", name); - } - - using (var stream = attachmentEntry.Open()) - { - await handler(stream); - } - - readAttachments++; - } - - public async Task ReadEventsAsync(IStreamNameResolver streamNameResolver, IEventDataFormatter formatter, Func<(string Stream, Envelope Event), Task> handler) - { - Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(formatter, nameof(formatter)); - Guard.NotNull(streamNameResolver, nameof(streamNameResolver)); - - while (true) - { - var eventEntry = archive.GetEntry(ArchiveHelper.GetEventPath(readEvents)); - - if (eventEntry == null) - { - break; - } - - using (var stream = eventEntry.Open()) - { - var (streamName, data) = serializer.Deserialize(stream).ToEvent(); - - MapHeaders(data); - - var eventStream = streamNameResolver.WithNewId(streamName, guidMapper.NewGuidOrNull); - var eventEnvelope = formatter.Parse(data, guidMapper.NewGuidOrValue); - - await handler((eventStream, eventEnvelope)); - } - - readEvents++; - } - } - - private void MapHeaders(EventData data) - { - foreach (var kvp in data.Headers.ToList()) - { - if (kvp.Value.Type == JsonValueType.String) - { - var newGuid = guidMapper.NewGuidOrNull(kvp.Value.ToString()); - - if (newGuid != null) - { - data.Headers.Add(kvp.Key, newGuid); - } - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs deleted file mode 100644 index 0f3033bcb..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs +++ /dev/null @@ -1,108 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.IO.Compression; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Backup.Helpers; -using Squidex.Domain.Apps.Entities.Backup.Model; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - public sealed class BackupWriter : DisposableObjectBase - { - private readonly ZipArchive archive; - private readonly IJsonSerializer serializer; - private readonly Func converter; - private int writtenEvents; - private int writtenAttachments; - - public int WrittenEvents - { - get { return writtenEvents; } - } - - public int WrittenAttachments - { - get { return writtenAttachments; } - } - - public BackupWriter(IJsonSerializer serializer, Stream stream, bool keepOpen = false, BackupVersion version = BackupVersion.V2) - { - Guard.NotNull(serializer, nameof(serializer)); - - this.serializer = serializer; - - converter = - version == BackupVersion.V1 ? - new Func(CompatibleStoredEvent.V1) : - new Func(CompatibleStoredEvent.V2); - - archive = new ZipArchive(stream, ZipArchiveMode.Create, keepOpen); - } - - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - archive.Dispose(); - } - } - - public Task WriteJsonAsync(string name, object value) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - var attachmentEntry = archive.CreateEntry(ArchiveHelper.GetAttachmentPath(name)); - - using (var stream = attachmentEntry.Open()) - { - serializer.Serialize(value, stream); - } - - writtenAttachments++; - - return TaskHelper.Done; - } - - public async Task WriteBlobAsync(string name, Func handler) - { - Guard.NotNullOrEmpty(name, nameof(name)); - Guard.NotNull(handler, nameof(handler)); - - var attachmentEntry = archive.CreateEntry(ArchiveHelper.GetAttachmentPath(name)); - - using (var stream = attachmentEntry.Open()) - { - await handler(stream); - } - - writtenAttachments++; - } - - public void WriteEvent(StoredEvent storedEvent) - { - Guard.NotNull(storedEvent, nameof(storedEvent)); - - var eventEntry = archive.CreateEntry(ArchiveHelper.GetEventPath(writtenEvents)); - - using (var stream = eventEntry.Open()) - { - var @event = converter(storedEvent); - - serializer.Serialize(@event, stream); - } - - writtenEvents++; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs b/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs deleted file mode 100644 index 830ace6a8..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs +++ /dev/null @@ -1,108 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - internal sealed class GuidMapper - { - private static readonly int GuidLength = Guid.Empty.ToString().Length; - private readonly Dictionary oldToNewGuid = new Dictionary(); - private readonly Dictionary newToOldGuid = new Dictionary(); - private readonly Dictionary strings = new Dictionary(); - - public Guid OldGuid(Guid newGuid) - { - return newToOldGuid.GetOrCreate(newGuid, x => x); - } - - public string NewGuidOrNull(string value) - { - if (TryGenerateNewGuidString(value, out var result) || TryGenerateNewNamedId(value, out result)) - { - return result; - } - - return null; - } - - public string NewGuidOrValue(string value) - { - if (TryGenerateNewGuidString(value, out var result) || TryGenerateNewNamedId(value, out result)) - { - return result; - } - - return value; - } - - private bool TryGenerateNewGuidString(string value, out string result) - { - if (value.Length == GuidLength) - { - if (strings.TryGetValue(value, out result)) - { - return true; - } - - if (Guid.TryParse(value, out var guid)) - { - var newGuid = GenerateNewGuid(guid); - - strings[value] = result = newGuid.ToString(); - - return true; - } - } - - result = null; - - return false; - } - - private bool TryGenerateNewNamedId(string value, out string result) - { - if (value.Length > GuidLength) - { - if (strings.TryGetValue(value, out result)) - { - return true; - } - - if (NamedId.TryParse(value, Guid.TryParse, out var namedId)) - { - var newGuid = GenerateNewGuid(namedId.Id); - - strings[value] = result = NamedId.Of(newGuid, namedId.Name).ToString(); - - return true; - } - } - - result = null; - - return false; - } - - private Guid GenerateNewGuid(Guid oldGuid) - { - return oldToNewGuid.GetOrAdd(oldGuid, GuidGenerator); - } - - private Guid GuidGenerator(Guid oldGuid) - { - var newGuid = Guid.NewGuid(); - - newToOldGuid[newGuid] = oldGuid; - - return newGuid; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs b/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs deleted file mode 100644 index 5a8aaff9f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; -using Squidex.Infrastructure.Json; - -namespace Squidex.Domain.Apps.Entities.Backup.Helpers -{ - public static class Downloader - { - public static async Task DownloadAsync(this IBackupArchiveLocation backupArchiveLocation, Uri url, string id) - { - if (string.Equals(url.Scheme, "file")) - { - try - { - using (var targetStream = await backupArchiveLocation.OpenStreamAsync(id)) - { - using (var sourceStream = new FileStream(url.LocalPath, FileMode.Open, FileAccess.Read)) - { - await sourceStream.CopyToAsync(targetStream); - } - } - } - catch (IOException ex) - { - throw new BackupRestoreException($"Cannot download the archive: {ex.Message}.", ex); - } - } - else - { - HttpResponseMessage response = null; - try - { - using (var client = new HttpClient()) - { - response = await client.GetAsync(url); - response.EnsureSuccessStatusCode(); - - using (var sourceStream = await response.Content.ReadAsStreamAsync()) - { - using (var targetStream = await backupArchiveLocation.OpenStreamAsync(id)) - { - await sourceStream.CopyToAsync(targetStream); - } - } - } - } - catch (HttpRequestException ex) - { - throw new BackupRestoreException($"Cannot download the archive. Got status code: {response?.StatusCode}.", ex); - } - } - } - - public static async Task OpenArchiveAsync(this IBackupArchiveLocation backupArchiveLocation, string id, IJsonSerializer serializer) - { - Stream stream = null; - - try - { - stream = await backupArchiveLocation.OpenStreamAsync(id); - - return new BackupReader(serializer, stream); - } - catch (IOException) - { - stream?.Dispose(); - - throw new BackupRestoreException("The backup archive is correupt and cannot be opened."); - } - catch (Exception) - { - stream?.Dispose(); - - throw; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs deleted file mode 100644 index 1fcc2ffb9..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - public interface IRestoreGrain : IGrainWithStringKey - { - Task RestoreAsync(Uri url, RefToken actor, string newAppName = null); - - Task> GetJobAsync(); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs deleted file mode 100644 index f7f1f0aef..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs +++ /dev/null @@ -1,367 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using NodaTime; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Backup.Helpers; -using Squidex.Domain.Apps.Entities.Backup.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - public sealed class RestoreGrain : GrainOfString, IRestoreGrain - { - private readonly IBackupArchiveLocation backupArchiveLocation; - private readonly IClock clock; - private readonly ICommandBus commandBus; - private readonly IJsonSerializer serializer; - private readonly IEventStore eventStore; - private readonly IEventDataFormatter eventDataFormatter; - private readonly ISemanticLog log; - private readonly IServiceProvider serviceProvider; - private readonly IStreamNameResolver streamNameResolver; - private readonly IGrainState state; - - private RestoreStateJob CurrentJob - { - get { return state.Value.Job; } - } - - public RestoreGrain(IBackupArchiveLocation backupArchiveLocation, - IClock clock, - ICommandBus commandBus, - IEventStore eventStore, - IEventDataFormatter eventDataFormatter, - IJsonSerializer serializer, - ISemanticLog log, - IServiceProvider serviceProvider, - IStreamNameResolver streamNameResolver, - IGrainState state) - { - Guard.NotNull(backupArchiveLocation, nameof(backupArchiveLocation)); - Guard.NotNull(clock, nameof(clock)); - Guard.NotNull(commandBus, nameof(commandBus)); - Guard.NotNull(eventStore, nameof(eventStore)); - Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); - Guard.NotNull(serializer, nameof(serializer)); - Guard.NotNull(serviceProvider, nameof(serviceProvider)); - Guard.NotNull(state, nameof(state)); - Guard.NotNull(streamNameResolver, nameof(streamNameResolver)); - Guard.NotNull(log, nameof(log)); - - this.backupArchiveLocation = backupArchiveLocation; - this.clock = clock; - this.commandBus = commandBus; - this.eventStore = eventStore; - this.eventDataFormatter = eventDataFormatter; - this.serializer = serializer; - this.serviceProvider = serviceProvider; - this.streamNameResolver = streamNameResolver; - this.state = state; - this.log = log; - } - - protected override Task OnActivateAsync(string key) - { - RecoverAfterRestartAsync().Forget(); - - return TaskHelper.Done; - } - - private async Task RecoverAfterRestartAsync() - { - if (CurrentJob?.Status == JobStatus.Started) - { - var handlers = CreateHandlers(); - - Log("Failed due application restart"); - - CurrentJob.Status = JobStatus.Failed; - - await CleanupAsync(handlers); - - await state.WriteAsync(); - } - } - - public async Task RestoreAsync(Uri url, RefToken actor, string newAppName) - { - Guard.NotNull(url, nameof(url)); - Guard.NotNull(actor, nameof(actor)); - - if (!string.IsNullOrWhiteSpace(newAppName)) - { - Guard.ValidSlug(newAppName, nameof(newAppName)); - } - - if (CurrentJob?.Status == JobStatus.Started) - { - throw new DomainException("A restore operation is already running."); - } - - state.Value.Job = new RestoreStateJob - { - Id = Guid.NewGuid(), - NewAppName = newAppName, - Actor = actor, - Started = clock.GetCurrentInstant(), - Status = JobStatus.Started, - Url = url - }; - - await state.WriteAsync(); - - Process(); - } - - private void Process() - { - ProcessAsync().Forget(); - } - - private async Task ProcessAsync() - { - var handlers = CreateHandlers(); - - var logContext = (jobId: CurrentJob.Id.ToString(), jobUrl: CurrentJob.Url.ToString()); - - using (Profiler.StartSession()) - { - try - { - Log("Started. The restore process has the following steps:"); - Log(" * Download backup"); - Log(" * Restore events and attachments."); - Log(" * Restore all objects like app, schemas and contents"); - Log(" * Complete the restore operation for all objects"); - - log.LogInformation(logContext, (ctx, w) => w - .WriteProperty("action", "restore") - .WriteProperty("status", "started") - .WriteProperty("operationId", ctx.jobId) - .WriteProperty("url", ctx.jobUrl)); - - using (Profiler.Trace("Download")) - { - await DownloadAsync(); - } - - using (var reader = await backupArchiveLocation.OpenArchiveAsync(CurrentJob.Id.ToString(), serializer)) - { - using (Profiler.Trace("ReadEvents")) - { - await ReadEventsAsync(reader, handlers); - } - - foreach (var handler in handlers) - { - using (Profiler.TraceMethod(handler.GetType(), nameof(BackupHandler.RestoreAsync))) - { - await handler.RestoreAsync(CurrentJob.AppId, reader); - } - - Log($"Restored {handler.Name}"); - } - - foreach (var handler in handlers) - { - using (Profiler.TraceMethod(handler.GetType(), nameof(BackupHandler.CompleteRestoreAsync))) - { - await handler.CompleteRestoreAsync(CurrentJob.AppId, reader); - } - - Log($"Completed {handler.Name}"); - } - } - - await AssignContributorAsync(); - - CurrentJob.Status = JobStatus.Completed; - - Log("Completed, Yeah!"); - - log.LogInformation(logContext, (ctx, w) => - { - w.WriteProperty("action", "restore"); - w.WriteProperty("status", "completed"); - w.WriteProperty("operationId", ctx.jobId); - w.WriteProperty("url", ctx.jobUrl); - - Profiler.Session?.Write(w); - }); - } - catch (Exception ex) - { - if (ex is BackupRestoreException backupException) - { - Log(backupException.Message); - } - else - { - Log("Failed with internal error"); - } - - await CleanupAsync(handlers); - - CurrentJob.Status = JobStatus.Failed; - - log.LogError(ex, logContext, (ctx, w) => - { - w.WriteProperty("action", "retore"); - w.WriteProperty("status", "failed"); - w.WriteProperty("operationId", ctx.jobId); - w.WriteProperty("url", ctx.jobUrl); - - Profiler.Session?.Write(w); - }); - } - finally - { - CurrentJob.Stopped = clock.GetCurrentInstant(); - - await state.WriteAsync(); - } - } - } - - private async Task AssignContributorAsync() - { - var actor = CurrentJob.Actor; - - if (actor?.IsSubject == true) - { - try - { - await commandBus.PublishAsync(new AssignContributor - { - Actor = actor, - AppId = CurrentJob.AppId, - ContributorId = actor.Identifier, - IsRestore = true, - Role = Role.Owner - }); - - Log("Assigned current user."); - } - catch (DomainException ex) - { - Log($"Failed to assign contributor: {ex.Message}"); - } - } - else - { - Log("Current user not assigned because restore was triggered by client."); - } - } - - private async Task CleanupAsync(IEnumerable handlers) - { - await Safe.DeleteAsync(backupArchiveLocation, CurrentJob.Id.ToString(), log); - - if (CurrentJob.AppId != Guid.Empty) - { - foreach (var handler in handlers) - { - await Safe.CleanupRestoreErrorAsync(handler, CurrentJob.AppId, CurrentJob.Id, log); - } - } - } - - private async Task DownloadAsync() - { - Log("Downloading Backup"); - - await backupArchiveLocation.DownloadAsync(CurrentJob.Url, CurrentJob.Id.ToString()); - - Log("Downloaded Backup"); - } - - private async Task ReadEventsAsync(BackupReader reader, IEnumerable handlers) - { - await reader.ReadEventsAsync(streamNameResolver, eventDataFormatter, async storedEvent => - { - await HandleEventAsync(reader, handlers, storedEvent.Stream, storedEvent.Event); - }); - - Log($"Reading {reader.ReadEvents} events and {reader.ReadAttachments} attachments completed.", true); - } - - private async Task HandleEventAsync(BackupReader reader, IEnumerable handlers, string stream, Envelope @event) - { - if (@event.Payload is SquidexEvent squidexEvent) - { - squidexEvent.Actor = CurrentJob.Actor; - } - - if (@event.Payload is AppCreated appCreated) - { - CurrentJob.AppId = appCreated.AppId.Id; - - if (!string.IsNullOrWhiteSpace(CurrentJob.NewAppName)) - { - appCreated.Name = CurrentJob.NewAppName; - } - } - - if (@event.Payload is AppEvent appEvent && !string.IsNullOrWhiteSpace(CurrentJob.NewAppName)) - { - appEvent.AppId = NamedId.Of(appEvent.AppId.Id, CurrentJob.NewAppName); - } - - foreach (var handler in handlers) - { - if (!await handler.RestoreEventAsync(@event, CurrentJob.AppId, reader, CurrentJob.Actor)) - { - return; - } - } - - var eventData = eventDataFormatter.ToEventData(@event, @event.Headers.CommitId()); - var eventCommit = new List { eventData }; - - await eventStore.AppendAsync(Guid.NewGuid(), stream, eventCommit); - - Log($"Read {reader.ReadEvents} events and {reader.ReadAttachments} attachments.", true); - } - - private void Log(string message, bool replace = false) - { - if (replace && CurrentJob.Log.Count > 0) - { - CurrentJob.Log[CurrentJob.Log.Count - 1] = $"{clock.GetCurrentInstant()}: {message}"; - } - else - { - CurrentJob.Log.Add($"{clock.GetCurrentInstant()}: {message}"); - } - } - - private IEnumerable CreateHandlers() - { - return serviceProvider.GetRequiredService>(); - } - - public Task> GetJobAsync() - { - return Task.FromResult>(CurrentJob); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs b/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs deleted file mode 100644 index 71abbdab7..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Runtime.Serialization; -using NodaTime; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Backup.State -{ - [DataContract] - public sealed class RestoreStateJob : IRestoreJob - { - [DataMember] - public string AppName { get; set; } - - [DataMember] - public Guid Id { get; set; } - - [DataMember] - public Guid AppId { get; set; } - - [DataMember] - public RefToken Actor { get; set; } - - [DataMember] - public Uri Url { get; set; } - - [DataMember] - public string NewAppName { get; set; } - - [DataMember] - public Instant Started { get; set; } - - [DataMember] - public Instant? Stopped { get; set; } - - [DataMember] - public List Log { get; set; } = new List(); - - [DataMember] - public JobStatus Status { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs b/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs deleted file mode 100644 index a90a2dc01..000000000 --- a/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs +++ /dev/null @@ -1,126 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Domain.Apps.Entities.Comments.Guards; -using Squidex.Domain.Apps.Entities.Comments.State; -using Squidex.Domain.Apps.Events.Comments; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Comments -{ - public sealed class CommentsGrain : DomainObjectGrainBase, ICommentsGrain - { - private readonly IStore store; - private readonly List> events = new List>(); - private CommentsState snapshot = new CommentsState { Version = EtagVersion.Empty }; - private IPersistence persistence; - - public override CommentsState Snapshot - { - get { return snapshot; } - } - - public CommentsGrain(IStore store, ISemanticLog log) - : base(log) - { - Guard.NotNull(store, nameof(store)); - - this.store = store; - } - - protected override void ApplyEvent(Envelope @event) - { - snapshot = new CommentsState { Version = snapshot.Version + 1 }; - - events.Add(@event.To()); - } - - protected override void RestorePreviousSnapshot(CommentsState previousSnapshot, long previousVersion) - { - snapshot = previousSnapshot; - } - - protected override Task ReadAsync(Type type, Guid id) - { - persistence = store.WithEventSourcing(GetType(), id, ApplyEvent); - - return persistence.ReadAsync(); - } - - protected override async Task WriteAsync(Envelope[] events, long previousVersion) - { - if (events.Length > 0) - { - await persistence.WriteEventsAsync(events); - } - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - switch (command) - { - case CreateComment createComment: - return UpsertReturn(createComment, c => - { - GuardComments.CanCreate(c); - - Create(c); - - return EntityCreatedResult.Create(createComment.CommentId, Version); - }); - - case UpdateComment updateComment: - return Upsert(updateComment, c => - { - GuardComments.CanUpdate(events, c); - - Update(c); - }); - - case DeleteComment deleteComment: - return Upsert(deleteComment, c => - { - GuardComments.CanDelete(events, c); - - Delete(c); - }); - - default: - throw new NotSupportedException(); - } - } - - public void Create(CreateComment command) - { - RaiseEvent(SimpleMapper.Map(command, new CommentCreated())); - } - - public void Update(UpdateComment command) - { - RaiseEvent(SimpleMapper.Map(command, new CommentUpdated())); - } - - public void Delete(DeleteComment command) - { - RaiseEvent(SimpleMapper.Map(command, new CommentDeleted())); - } - - public Task GetCommentsAsync(long version = EtagVersion.Any) - { - return Task.FromResult(CommentsResult.FromEvents(events, Version, (int)version)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs b/src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs deleted file mode 100644 index 1a2921e67..000000000 --- a/src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs +++ /dev/null @@ -1,89 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Domain.Apps.Events.Comments; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Comments.Guards -{ - public static class GuardComments - { - public static void CanCreate(CreateComment command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot create comment.", e => - { - if (string.IsNullOrWhiteSpace(command.Text)) - { - e(Not.Defined("Text"), nameof(command.Text)); - } - }); - } - - public static void CanUpdate(List> events, UpdateComment command) - { - Guard.NotNull(command, nameof(command)); - - var comment = FindComment(events, command.CommentId); - - if (!comment.Payload.Actor.Equals(command.Actor)) - { - throw new DomainException("Comment is created by another actor."); - } - - Validate.It(() => "Cannot update comment.", e => - { - if (string.IsNullOrWhiteSpace(command.Text)) - { - e(Not.Defined("Text"), nameof(command.Text)); - } - }); - } - - public static void CanDelete(List> events, DeleteComment command) - { - Guard.NotNull(command, nameof(command)); - - var comment = FindComment(events, command.CommentId); - - if (!comment.Payload.Actor.Equals(command.Actor)) - { - throw new DomainException("Comment is created by another actor."); - } - } - - private static Envelope FindComment(List> events, Guid commentId) - { - Envelope result = null; - - foreach (var @event in events) - { - if (@event.Payload is CommentCreated created && created.CommentId == commentId) - { - result = @event.To(); - } - else if (@event.Payload is CommentDeleted deleted && deleted.CommentId == commentId) - { - result = null; - } - } - - if (result == null) - { - throw new DomainObjectNotFoundException(commentId.ToString(), "Comments", typeof(CommentsGrain)); - } - - return result; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs deleted file mode 100644 index 30167fe77..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ /dev/null @@ -1,133 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// 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.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentChangedTriggerHandler : RuleTriggerHandler - { - private readonly IScriptEngine scriptEngine; - private readonly IContentLoader contentLoader; - - public ContentChangedTriggerHandler(IScriptEngine scriptEngine, IContentLoader contentLoader) - { - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(contentLoader, nameof(contentLoader)); - - this.scriptEngine = scriptEngine; - - this.contentLoader = contentLoader; - } - - protected override async Task CreateEnrichedEventAsync(Envelope @event) - { - var result = new EnrichedContentEvent(); - - var content = await contentLoader.GetAsync(@event.Headers.AggregateId(), @event.Headers.EventStreamNumber()); - - SimpleMapper.Map(content, result); - - result.Data = content.Data ?? content.DataDraft; - - switch (@event.Payload) - { - case ContentCreated _: - result.Type = EnrichedContentEventType.Created; - break; - case ContentDeleted _: - result.Type = EnrichedContentEventType.Deleted; - break; - case ContentChangesPublished _: - case ContentUpdated _: - result.Type = EnrichedContentEventType.Updated; - break; - case ContentStatusChanged contentStatusChanged: - switch (contentStatusChanged.Change) - { - case StatusChange.Published: - result.Type = EnrichedContentEventType.Published; - break; - case StatusChange.Unpublished: - result.Type = EnrichedContentEventType.Unpublished; - break; - default: - result.Type = EnrichedContentEventType.StatusChanged; - break; - } - - break; - } - - result.Name = $"{content.SchemaId.Name.ToPascalCase()}{result.Type}"; - - return result; - } - - protected override bool Trigger(ContentEvent @event, ContentChangedTriggerV2 trigger, Guid ruleId) - { - if (trigger.HandleAll) - { - return true; - } - - if (trigger.Schemas != null) - { - foreach (var schema in trigger.Schemas) - { - if (MatchsSchema(schema, @event.SchemaId)) - { - return true; - } - } - } - - return false; - } - - protected override bool Trigger(EnrichedContentEvent @event, ContentChangedTriggerV2 trigger) - { - if (trigger.HandleAll) - { - return true; - } - - if (trigger.Schemas != null) - { - foreach (var schema in trigger.Schemas) - { - if (MatchsSchema(schema, @event.SchemaId) && MatchsCondition(schema, @event)) - { - return true; - } - } - } - - return false; - } - - private static bool MatchsSchema(ContentChangedTriggerSchemaV2 schema, NamedId eventId) - { - return eventId.Id == schema.SchemaId; - } - - private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEventBase @event) - { - return string.IsNullOrWhiteSpace(schema.Condition) || scriptEngine.Evaluate("event", @event, schema.Condition); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs deleted file mode 100644 index 4ebe87d7a..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentCommandMiddleware : GrainCommandMiddleware - { - private readonly IContentEnricher contentEnricher; - private readonly IContextProvider contextProvider; - - public ContentCommandMiddleware(IGrainFactory grainFactory, IContentEnricher contentEnricher, IContextProvider contextProvider) - : base(grainFactory) - { - Guard.NotNull(contentEnricher, nameof(contentEnricher)); - Guard.NotNull(contextProvider, nameof(contextProvider)); - - this.contentEnricher = contentEnricher; - this.contextProvider = contextProvider; - } - - public override async Task HandleAsync(CommandContext context, Func next) - { - await base.HandleAsync(context, next); - - if (context.PlainResult is IContentEntity content && NotEnriched(context)) - { - var enriched = await contentEnricher.EnrichAsync(content, contextProvider.Context); - - context.Complete(enriched); - } - } - - private static bool NotEnriched(CommandContext context) - { - return !(context.PlainResult is IEnrichedContentEntity); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs deleted file mode 100644 index b8c0ded1c..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentEntity : IEnrichedContentEntity - { - public Guid Id { get; set; } - - public NamedId AppId { get; set; } - - public NamedId SchemaId { get; set; } - - public long Version { get; set; } - - public Instant Created { get; set; } - - public Instant LastModified { get; set; } - - public RefToken CreatedBy { get; set; } - - public RefToken LastModifiedBy { get; set; } - - public ScheduleJob ScheduleJob { get; set; } - - public NamedContentData Data { get; set; } - - public NamedContentData DataDraft { get; set; } - - public NamedContentData ReferenceData { get; set; } - - public Status Status { get; set; } - - public StatusInfo[] Nexts { get; set; } - - public string StatusColor { get; set; } - - public string SchemaName { get; set; } - - public string SchemaDisplayName { get; set; } - - public RootField[] ReferenceFields { get; set; } - - public bool CanUpdate { get; set; } - - public bool IsPending { get; set; } - - public HashSet CacheDependencies { get; set; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs deleted file mode 100644 index 0de01a4a2..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ /dev/null @@ -1,377 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// 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.Scripting; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.Guards; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Contents.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentGrain : LogSnapshotDomainObjectGrain, IContentGrain - { - private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); - private readonly IAppProvider appProvider; - private readonly IAssetRepository assetRepository; - private readonly IContentRepository contentRepository; - private readonly IScriptEngine scriptEngine; - private readonly IContentWorkflow contentWorkflow; - - public ContentGrain( - IStore store, - ISemanticLog log, - IAppProvider appProvider, - IAssetRepository assetRepository, - IScriptEngine scriptEngine, - IContentWorkflow contentWorkflow, - IContentRepository contentRepository, - IActivationLimit limit) - : base(store, log) - { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(assetRepository, nameof(assetRepository)); - Guard.NotNull(contentWorkflow, nameof(contentWorkflow)); - Guard.NotNull(contentRepository, nameof(contentRepository)); - - this.appProvider = appProvider; - this.scriptEngine = scriptEngine; - this.assetRepository = assetRepository; - this.contentWorkflow = contentWorkflow; - this.contentRepository = contentRepository; - - limit?.SetLimit(5000, Lifetime); - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - VerifyNotDeleted(); - - switch (command) - { - case CreateContent createContent: - return CreateReturnAsync(createContent, async c => - { - var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, c, () => "Failed to create content."); - - var status = (await contentWorkflow.GetInitialStatusAsync(ctx.Schema)).Status; - - await GuardContent.CanCreate(ctx.Schema, contentWorkflow, c); - - c.Data = await ctx.ExecuteScriptAndTransformAsync(s => s.Create, - new ScriptContext - { - Operation = "Create", - Data = c.Data, - Status = status, - StatusOld = default - }); - - await ctx.EnrichAsync(c.Data); - - if (!c.DoNotValidate) - { - await ctx.ValidateAsync(c.Data); - } - - if (c.Publish) - { - await ctx.ExecuteScriptAsync(s => s.Change, - new ScriptContext - { - Operation = "Published", - Data = c.Data, - Status = Status.Published, - StatusOld = default - }); - } - - Create(c, status); - - return Snapshot; - }); - - case UpdateContent updateContent: - return UpdateReturnAsync(updateContent, async c => - { - var isProposal = c.AsDraft && Snapshot.Status == Status.Published; - - await GuardContent.CanUpdate(Snapshot, contentWorkflow, c, isProposal); - - return await UpdateAsync(c, x => c.Data, false, isProposal); - }); - - case PatchContent patchContent: - return UpdateReturnAsync(patchContent, async c => - { - var isProposal = IsProposal(c); - - await GuardContent.CanPatch(Snapshot, contentWorkflow, c, isProposal); - - return await UpdateAsync(c, c.Data.MergeInto, true, isProposal); - }); - - case ChangeContentStatus changeContentStatus: - return UpdateReturnAsync(changeContentStatus, async c => - { - try - { - var isChangeConfirm = IsConfirm(c); - - var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to change content."); - - await GuardContent.CanChangeStatus(ctx.Schema, Snapshot, contentWorkflow, c, isChangeConfirm); - - if (c.DueTime.HasValue) - { - ScheduleStatus(c); - } - else - { - if (isChangeConfirm) - { - ConfirmChanges(c); - } - else - { - var change = GetChange(c); - - await ctx.ExecuteScriptAsync(s => s.Change, - new ScriptContext - { - Operation = change.ToString(), - Data = Snapshot.Data, - Status = c.Status, - StatusOld = Snapshot.Status - }); - - ChangeStatus(c, change); - } - } - } - catch (Exception) - { - if (c.JobId.HasValue && Snapshot?.ScheduleJob.Id == c.JobId) - { - CancelScheduling(c); - } - else - { - throw; - } - } - - return Snapshot; - }); - - case DiscardChanges discardChanges: - return UpdateReturn(discardChanges, c => - { - GuardContent.CanDiscardChanges(Snapshot.IsPending, c); - - DiscardChanges(c); - - return Snapshot; - }); - - case DeleteContent deleteContent: - return UpdateAsync(deleteContent, async c => - { - var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to delete content."); - - GuardContent.CanDelete(ctx.Schema, c); - - await ctx.ExecuteScriptAsync(s => s.Delete, - new ScriptContext - { - Operation = "Delete", - Data = Snapshot.Data, - Status = Snapshot.Status, - StatusOld = default - }); - - Delete(c); - }); - - default: - throw new NotSupportedException(); - } - } - - private async Task UpdateAsync(ContentUpdateCommand command, Func newDataFunc, bool partial, bool isProposal) - { - var currentData = - isProposal ? - Snapshot.DataDraft : - Snapshot.Data; - - var newData = newDataFunc(currentData); - - if (!currentData.Equals(newData)) - { - var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, command, () => "Failed to update content."); - - if (partial) - { - await ctx.ValidatePartialAsync(command.Data); - } - else - { - await ctx.ValidateAsync(command.Data); - } - - newData = await ctx.ExecuteScriptAndTransformAsync(s => s.Update, - new ScriptContext - { - Operation = "Create", - Data = newData, - DataOld = currentData, - Status = Snapshot.Status, - StatusOld = default - }); - - if (isProposal) - { - ProposeUpdate(command, newData); - } - else - { - Update(command, newData); - } - } - - return Snapshot; - } - - public void Create(CreateContent command, Status status) - { - RaiseEvent(SimpleMapper.Map(command, new ContentCreated { Status = status })); - - if (command.Publish) - { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published })); - } - } - - public void ConfirmChanges(ChangeContentStatus command) - { - RaiseEvent(SimpleMapper.Map(command, new ContentChangesPublished())); - } - - public void DiscardChanges(DiscardChanges command) - { - RaiseEvent(SimpleMapper.Map(command, new ContentChangesDiscarded())); - } - - public void Delete(DeleteContent command) - { - RaiseEvent(SimpleMapper.Map(command, new ContentDeleted())); - } - - public void Update(ContentCommand command, NamedContentData data) - { - RaiseEvent(SimpleMapper.Map(command, new ContentUpdated { Data = data })); - } - - public void ProposeUpdate(ContentCommand command, NamedContentData data) - { - RaiseEvent(SimpleMapper.Map(command, new ContentUpdateProposed { Data = data })); - } - - public void CancelScheduling(ChangeContentStatus command) - { - RaiseEvent(SimpleMapper.Map(command, new ContentSchedulingCancelled())); - } - - public void ScheduleStatus(ChangeContentStatus command) - { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value })); - } - - public void ChangeStatus(ChangeContentStatus command, StatusChange change) - { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = change })); - } - - private void RaiseEvent(SchemaEvent @event) - { - if (@event.AppId == null) - { - @event.AppId = Snapshot.AppId; - } - - if (@event.SchemaId == null) - { - @event.SchemaId = Snapshot.SchemaId; - } - - RaiseEvent(Envelope.Create(@event)); - } - - private bool IsConfirm(ChangeContentStatus command) - { - return Snapshot.IsPending && Snapshot.Status == Status.Published && command.Status == Status.Published; - } - - private bool IsProposal(PatchContent command) - { - return Snapshot.Status == Status.Published && command.AsDraft; - } - - private StatusChange GetChange(ChangeContentStatus command) - { - var change = StatusChange.Change; - - if (command.Status == Status.Published) - { - change = StatusChange.Published; - } - else if (Snapshot.Status == Status.Published) - { - change = StatusChange.Unpublished; - } - - return change; - } - - private void VerifyNotDeleted() - { - if (Snapshot.IsDeleted) - { - throw new DomainException("Content has already been deleted."); - } - } - - private async Task CreateContext(Guid appId, Guid schemaId, ContentCommand command, Func message) - { - var operationContext = - await ContentOperationContext.CreateAsync(appId, schemaId, command, - appProvider, assetRepository, contentRepository, scriptEngine, message); - - return operationContext; - } - - public Task> GetStateAsync(long version = EtagVersion.Any) - { - return J.AsTask(GetSnapshot(version)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs deleted file mode 100644 index c94213680..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.History; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase - { - public ContentHistoryEventsCreator(TypeNameRegistry typeNameRegistry) - : base(typeNameRegistry) - { - AddEventMessage( - "created {[Schema]} content."); - - AddEventMessage( - "updated {[Schema]} content."); - - AddEventMessage( - "deleted {[Schema]} content."); - - AddEventMessage( - "discarded pending changes of {[Schema]} content."); - - AddEventMessage( - "published changes of {[Schema]} content."); - - AddEventMessage( - "proposed update for {[Schema]} content."); - - AddEventMessage( - "failed to schedule status change for {[Schema]} content."); - - AddEventMessage( - "changed status of {[Schema]} content to {[Status]}."); - - AddEventMessage( - "scheduled to change status of {[Schema]} content to {[Status]}."); - } - - protected override Task CreateEventCoreAsync(Envelope @event) - { - var channel = $"contents.{@event.Headers.AggregateId()}"; - - var result = ForEvent(@event.Payload, channel); - - if (@event.Payload is SchemaEvent schemaEvent) - { - result = result.Param("Schema", schemaEvent.SchemaId.Name); - } - - if (@event.Payload is ContentStatusChanged contentStatusChanged) - { - result = result.Param("Status", contentStatusChanged.Status); - } - - if (@event.Payload is ContentStatusScheduled contentStatusScheduled) - { - result = result.Param("Status", contentStatusScheduled.Status); - } - - return Task.FromResult(result); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs deleted file mode 100644 index d5752543a..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs +++ /dev/null @@ -1,143 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.EnrichContent; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentOperationContext - { - private IContentRepository contentRepository; - private IAssetRepository assetRepository; - private IScriptEngine scriptEngine; - private ISchemaEntity schemaEntity; - private IAppEntity appEntity; - private ContentCommand command; - private Guid schemaId; - private Func message; - - public ISchemaEntity Schema - { - get { return schemaEntity; } - } - - public static async Task CreateAsync( - Guid appId, - Guid schemaId, - ContentCommand command, - IAppProvider appProvider, - IAssetRepository assetRepository, - IContentRepository contentRepository, - IScriptEngine scriptEngine, - Func message) - { - var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(appId, schemaId); - - var context = new ContentOperationContext - { - appEntity = appEntity, - assetRepository = assetRepository, - command = command, - contentRepository = contentRepository, - message = message, - schemaId = schemaId, - schemaEntity = schemaEntity, - scriptEngine = scriptEngine - }; - - return context; - } - - public Task EnrichAsync(NamedContentData data) - { - data.Enrich(schemaEntity.SchemaDef, appEntity.PartitionResolver()); - - return TaskHelper.Done; - } - - public Task ValidateAsync(NamedContentData data) - { - var ctx = CreateValidationContext(); - - return data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); - } - - public Task ValidatePartialAsync(NamedContentData data) - { - var ctx = CreateValidationContext(); - - return data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); - } - - public Task ExecuteScriptAndTransformAsync(Func script, ScriptContext context) - { - Enrich(context); - - var result = scriptEngine.ExecuteAndTransform(context, GetScript(script)); - - return Task.FromResult(result); - } - - public Task ExecuteScriptAsync(Func script, ScriptContext context) - { - Enrich(context); - - scriptEngine.Execute(context, GetScript(script)); - - return TaskHelper.Done; - } - - private void Enrich(ScriptContext context) - { - context.ContentId = command.ContentId; - - context.User = command.User; - } - - private ValidationContext CreateValidationContext() - { - return new ValidationContext(command.ContentId, schemaId, - QueryContentsAsync, - QueryContentsAsync, - QueryAssetsAsync); - } - - private async Task> QueryAssetsAsync(IEnumerable assetIds) - { - return await assetRepository.QueryAsync(appEntity.Id, new HashSet(assetIds)); - } - - private async Task> QueryContentsAsync(Guid filterSchemaId, FilterNode filterNode) - { - return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode); - } - - private async Task> QueryContentsAsync(HashSet ids) - { - return await contentRepository.QueryIdsAsync(appEntity.Id, ids); - } - - private string GetScript(Func script) - { - return script(schemaEntity.SchemaDef.Scripts); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs deleted file mode 100644 index 051f66a7a..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using NodaTime; -using Orleans; -using Orleans.Runtime; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentSchedulerGrain : Grain, IContentSchedulerGrain, IRemindable - { - private readonly IContentRepository contentRepository; - private readonly ICommandBus commandBus; - private readonly IClock clock; - private readonly ISemanticLog log; - private TaskScheduler scheduler; - - public ContentSchedulerGrain( - IContentRepository contentRepository, - ICommandBus commandBus, - IClock clock, - ISemanticLog log) - { - Guard.NotNull(contentRepository, nameof(contentRepository)); - Guard.NotNull(commandBus, nameof(commandBus)); - Guard.NotNull(clock, nameof(clock)); - Guard.NotNull(log, nameof(log)); - - this.clock = clock; - - this.commandBus = commandBus; - this.contentRepository = contentRepository; - - this.log = log; - } - - public override Task OnActivateAsync() - { - scheduler = TaskScheduler.Current; - - DelayDeactivation(TimeSpan.FromDays(1)); - - RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); - RegisterTimer(x => PublishAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); - - return Task.FromResult(true); - } - - public Task ActivateAsync() - { - return TaskHelper.Done; - } - - public Task PublishAsync() - { - var now = clock.GetCurrentInstant(); - - return contentRepository.QueryScheduledWithoutDataAsync(now, content => - { - return Dispatch(async () => - { - try - { - var job = content.ScheduleJob; - - if (job != null) - { - var command = new ChangeContentStatus { ContentId = content.Id, Status = job.Status, Actor = job.ScheduledBy, JobId = job.Id }; - - await commandBus.PublishAsync(command); - } - } - catch (Exception ex) - { - log.LogError(ex, content.Id.ToString(), (logContentId, w) => w - .WriteProperty("action", "ChangeStatusScheduled") - .WriteProperty("status", "Failed") - .WriteProperty("contentId", logContentId)); - } - }); - }); - } - - public Task ReceiveReminder(string reminderName, TickStatus status) - { - return TaskHelper.Done; - } - - private Task Dispatch(Func task) - { - return Task.Factory.StartNew(task, CancellationToken.None, TaskCreationOptions.None, scheduler ?? TaskScheduler.Default).Unwrap(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs b/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs deleted file mode 100644 index 75ddd704b..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class DefaultWorkflowsValidator : IWorkflowsValidator - { - private readonly IAppProvider appProvider; - - public DefaultWorkflowsValidator(IAppProvider appProvider) - { - Guard.NotNull(appProvider, nameof(appProvider)); - - this.appProvider = appProvider; - } - - public async Task> ValidateAsync(Guid appId, Workflows workflows) - { - Guard.NotNull(workflows, nameof(workflows)); - - var errors = new List(); - - if (workflows.Values.Count(x => x.SchemaIds.Count == 0) > 1) - { - errors.Add("Multiple workflows cover all schemas."); - } - - var uniqueSchemaIds = workflows.Values.SelectMany(x => x.SchemaIds).Distinct().ToList(); - - foreach (var schemaId in uniqueSchemaIds) - { - if (workflows.Values.Count(x => x.SchemaIds.Contains(schemaId)) > 1) - { - var schema = await appProvider.GetSchemaAsync(appId, schemaId); - - if (schema != null) - { - errors.Add($"The schema `{schema.SchemaDef.Name}` is covered by multiple workflows."); - } - } - } - - return errors; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs deleted file mode 100644 index 6ae3f9e58..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs +++ /dev/null @@ -1,153 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class DynamicContentWorkflow : IContentWorkflow - { - private readonly IScriptEngine scriptEngine; - private readonly IAppProvider appProvider; - - public DynamicContentWorkflow(IScriptEngine scriptEngine, IAppProvider appProvider) - { - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(appProvider, nameof(appProvider)); - - this.scriptEngine = scriptEngine; - - this.appProvider = appProvider; - } - - public async Task GetAllAsync(ISchemaEntity schema) - { - var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); - - return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray(); - } - - public async Task CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user) - { - var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); - - return workflow.TryGetTransition(content.Status, next, out var transition) && CanUse(transition, content.DataDraft, user); - } - - public async Task CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user) - { - var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); - - return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && CanUse(transition, data, user); - } - - public async Task CanUpdateAsync(IContentEntity content) - { - var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); - - if (workflow.TryGetStep(content.Status, out var step)) - { - return !step.NoUpdate; - } - - return true; - } - - public async Task GetInfoAsync(IContentEntity content) - { - var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); - - if (workflow.TryGetStep(content.Status, out var step)) - { - return new StatusInfo(content.Status, GetColor(step)); - } - - return new StatusInfo(content.Status, StatusColors.Draft); - } - - public async Task GetInitialStatusAsync(ISchemaEntity schema) - { - var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); - - var (status, step) = workflow.GetInitialStep(); - - return new StatusInfo(status, GetColor(step)); - } - - public async Task GetNextsAsync(IContentEntity content, ClaimsPrincipal user) - { - var result = new List(); - - var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); - - foreach (var (to, step, transition) in workflow.GetTransitions(content.Status)) - { - if (CanUse(transition, content.DataDraft, user)) - { - result.Add(new StatusInfo(to, GetColor(step))); - } - } - - return result.ToArray(); - } - - private bool CanUse(WorkflowTransition transition, NamedContentData data, ClaimsPrincipal user) - { - if (transition.Roles != null) - { - if (!user.Claims.Any(x => x.Type == ClaimTypes.Role && transition.Roles.Contains(x.Value))) - { - return false; - } - } - - if (!string.IsNullOrWhiteSpace(transition.Expression)) - { - return scriptEngine.Evaluate("data", data, transition.Expression); - } - - return true; - } - - private async Task GetWorkflowAsync(Guid appId, Guid schemaId) - { - Workflow result = null; - - var app = await appProvider.GetAppAsync(appId); - - if (app != null) - { - result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Contains(schemaId)); - - if (result == null) - { - result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Count == 0); - } - } - - if (result == null) - { - result = Workflow.Default; - } - - return result; - } - - private static string GetColor(WorkflowStep step) - { - return step.Color ?? StatusColors.Draft; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs deleted file mode 100644 index da2e74af8..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ /dev/null @@ -1,114 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using GraphQL; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Caching; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService - { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); - private readonly IDependencyResolver resolver; - - public CachingGraphQLService(IMemoryCache cache, IDependencyResolver resolver) - : base(cache) - { - Guard.NotNull(resolver, nameof(resolver)); - - this.resolver = resolver; - } - - public async Task<(bool HasError, object Response)> QueryAsync(Context context, params GraphQLQuery[] queries) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(queries, nameof(queries)); - - var model = await GetModelAsync(context.App); - - var ctx = new GraphQLExecutionContext(context, resolver); - - var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, ctx, q))); - - return (result.Any(x => x.HasError), result.Map(x => x.Response)); - } - - public async Task<(bool HasError, object Response)> QueryAsync(Context context, GraphQLQuery query) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(query, nameof(query)); - - var model = await GetModelAsync(context.App); - - var ctx = new GraphQLExecutionContext(context, resolver); - - var result = await QueryInternalAsync(model, ctx, query); - - return result; - } - - private static async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext ctx, GraphQLQuery query) - { - if (string.IsNullOrWhiteSpace(query.Query)) - { - return (false, new { data = new object() }); - } - - var (data, errors) = await model.ExecuteAsync(ctx, query); - - if (errors?.Any() == true) - { - return (false, new { data, errors }); - } - else - { - return (false, new { data }); - } - } - - private Task GetModelAsync(IAppEntity app) - { - var cacheKey = CreateCacheKey(app.Id, app.Version.ToString()); - - return Cache.GetOrCreateAsync(cacheKey, async entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheDuration; - - var allSchemas = await resolver.Resolve().GetSchemasAsync(app.Id); - - return new GraphQLModel(app, - allSchemas, - GetPageSizeForContents(), - GetPageSizeForAssets(), - resolver.Resolve()); - }); - } - - private int GetPageSizeForContents() - { - return resolver.Resolve>().Value.DefaultPageSizeGraphQl; - } - - private int GetPageSizeForAssets() - { - return resolver.Resolve>().Value.DefaultPageSizeGraphQl; - } - - private static object CreateCacheKey(Guid appId, string etag) - { - return $"GraphQLModel_{appId}_{etag}"; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs deleted file mode 100644 index f5a27ffd9..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ /dev/null @@ -1,142 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using GraphQL; -using GraphQL.DataLoader; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; -using Squidex.Domain.Apps.Entities.Contents.Queries; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Log; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public sealed class GraphQLExecutionContext : QueryExecutionContext - { - private static readonly List EmptyAssets = new List(); - private static readonly List EmptyContents = new List(); - private readonly IDataLoaderContextAccessor dataLoaderContextAccessor; - private readonly IDependencyResolver resolver; - - public IGraphQLUrlGenerator UrlGenerator { get; } - - public ISemanticLog Log { get; } - - public GraphQLExecutionContext(Context context, IDependencyResolver resolver) - : base(context, - resolver.Resolve(), - resolver.Resolve()) - { - UrlGenerator = resolver.Resolve(); - - dataLoaderContextAccessor = resolver.Resolve(); - - this.resolver = resolver; - } - - public void Setup(ExecutionOptions execution) - { - var loader = resolver.Resolve(); - - execution.Listeners.Add(loader); - execution.FieldMiddleware.Use(Middlewares.Logging(resolver.Resolve())); - execution.FieldMiddleware.Use(Middlewares.Errors()); - - execution.UserContext = this; - } - - public override Task FindAssetAsync(Guid id) - { - var dataLoader = GetAssetsLoader(); - - return dataLoader.LoadAsync(id); - } - - public Task FindContentAsync(Guid id) - { - var dataLoader = GetContentsLoader(); - - return dataLoader.LoadAsync(id); - } - - public async Task> GetReferencedAssetsAsync(IJsonValue value) - { - var ids = ParseIds(value); - - if (ids == null) - { - return EmptyAssets; - } - - var dataLoader = GetAssetsLoader(); - - return await dataLoader.LoadManyAsync(ids); - } - - public async Task> GetReferencedContentsAsync(IJsonValue value) - { - var ids = ParseIds(value); - - if (ids == null) - { - return EmptyContents; - } - - var dataLoader = GetContentsLoader(); - - return await dataLoader.LoadManyAsync(ids); - } - - private IDataLoader GetAssetsLoader() - { - return dataLoaderContextAccessor.Context.GetOrAddBatchLoader("Assets", - async batch => - { - var result = await GetReferencedAssetsAsync(new List(batch)); - - return result.ToDictionary(x => x.Id); - }); - } - - private IDataLoader GetContentsLoader() - { - return dataLoaderContextAccessor.Context.GetOrAddBatchLoader($"References", - async batch => - { - var result = await GetReferencedContentsAsync(new List(batch)); - - return result.ToDictionary(x => x.Id); - }); - } - - private static ICollection ParseIds(IJsonValue value) - { - try - { - var result = new List(); - - if (value is JsonArray array) - { - foreach (var id in array) - { - result.Add(Guid.Parse(id.ToString())); - } - } - - return result; - } - catch - { - return null; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs deleted file mode 100644 index 151264b20..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs +++ /dev/null @@ -1,180 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using GraphQL; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; -using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using GraphQLSchema = GraphQL.Types.Schema; - -#pragma warning disable IDE0003 - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public sealed class GraphQLModel : IGraphModel - { - private readonly Dictionary contentTypes = new Dictionary(); - private readonly PartitionResolver partitionResolver; - private readonly IAppEntity app; - private readonly IGraphType assetType; - private readonly IGraphType assetListType; - private readonly GraphQLSchema graphQLSchema; - - public bool CanGenerateAssetSourceUrl { get; } - - public GraphQLModel(IAppEntity app, - IEnumerable schemas, - int pageSizeContents, - int pageSizeAssets, - IGraphQLUrlGenerator urlGenerator) - { - this.app = app; - - partitionResolver = app.PartitionResolver(); - - CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl; - - assetType = new AssetGraphType(this); - assetListType = new ListGraphType(new NonNullGraphType(assetType)); - - var allSchemas = schemas.Where(x => x.SchemaDef.IsPublished).ToList(); - - BuildSchemas(allSchemas); - - graphQLSchema = BuildSchema(this, pageSizeContents, pageSizeAssets, allSchemas); - graphQLSchema.RegisterValueConverter(JsonConverter.Instance); - - InitializeContentTypes(); - } - - private void BuildSchemas(List allSchemas) - { - foreach (var schema in allSchemas) - { - contentTypes[schema.Id] = new ContentGraphType(schema); - } - } - - private void InitializeContentTypes() - { - foreach (var contentType in contentTypes.Values) - { - contentType.Initialize(this); - } - - foreach (var contentType in contentTypes.Values) - { - graphQLSchema.RegisterType(contentType); - } - } - - private static GraphQLSchema BuildSchema(GraphQLModel model, int pageSizeContents, int pageSizeAssets, List schemas) - { - var schema = new GraphQLSchema - { - Query = new AppQueriesGraphType(model, pageSizeContents, pageSizeAssets, schemas) - }; - - return schema; - } - - public IFieldResolver ResolveAssetUrl() - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.UrlGenerator.GenerateAssetUrl(app, c.Source); - }); - - return resolver; - } - - public IFieldResolver ResolveAssetSourceUrl() - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.UrlGenerator.GenerateAssetSourceUrl(app, c.Source); - }); - - return resolver; - } - - public IFieldResolver ResolveAssetThumbnailUrl() - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.UrlGenerator.GenerateAssetThumbnailUrl(app, c.Source); - }); - - return resolver; - } - - public IFieldResolver ResolveContentUrl(ISchemaEntity schema) - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.UrlGenerator.GenerateContentUrl(app, schema, c.Source); - }); - - return resolver; - } - - public IFieldPartitioning ResolvePartition(Partitioning key) - { - return partitionResolver(key); - } - - public (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName) - { - return field.Accept(new QueryGraphTypeVisitor(schema, contentTypes, this, assetListType, fieldName)); - } - - public IObjectGraphType GetAssetType() - { - return assetType as IObjectGraphType; - } - - public IObjectGraphType GetContentType(Guid schemaId) - { - return contentTypes.GetOrDefault(schemaId); - } - - public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query) - { - Guard.NotNull(context, nameof(context)); - - var result = await new DocumentExecuter().ExecuteAsync(execution => - { - context.Setup(execution); - - execution.Schema = graphQLSchema; - execution.Inputs = query.Variables?.ToInputs(); - execution.Query = query.Query; - }).ConfigureAwait(false); - - return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray()); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs deleted file mode 100644 index d945c3a83..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; -using Squidex.Domain.Apps.Entities.Schemas; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public interface IGraphModel - { - bool CanGenerateAssetSourceUrl { get; } - - IFieldPartitioning ResolvePartition(Partitioning key); - - IFieldResolver ResolveAssetUrl(); - - IFieldResolver ResolveAssetSourceUrl(); - - IFieldResolver ResolveAssetThumbnailUrl(); - - IFieldResolver ResolveContentUrl(ISchemaEntity schema); - - IObjectGraphType GetAssetType(); - - IObjectGraphType GetContentType(Guid schemaId); - - (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs deleted file mode 100644 index 48afc4d4e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Schemas; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public interface IGraphQLUrlGenerator - { - bool CanGenerateAssetSourceUrl { get; } - - string GenerateAssetUrl(IAppEntity app, IAssetEntity asset); - - string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset); - - string GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset); - - string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs deleted file mode 100644 index 03adccccc..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using GraphQL; -using GraphQL.Instrumentation; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public static class Middlewares - { - public static Func Logging(ISemanticLog log) - { - Guard.NotNull(log, nameof(log)); - - return next => - { - return async context => - { - try - { - return await next(context); - } - catch (Exception ex) - { - log.LogWarning(ex, w => w - .WriteProperty("action", "reolveField") - .WriteProperty("status", "failed") - .WriteProperty("field", context.FieldName)); - - throw; - } - }; - }; - } - - public static Func Errors() - { - return next => - { - return async context => - { - try - { - return await next(context); - } - catch (DomainException ex) - { - throw new ExecutionError(ex.Message); - } - }; - }; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs deleted file mode 100644 index 639ee6d55..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs +++ /dev/null @@ -1,194 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types -{ - public sealed class AssetGraphType : ObjectGraphType - { - public AssetGraphType(IGraphModel model) - { - Name = "Asset"; - - AddField(new FieldType - { - Name = "id", - ResolvedType = AllTypes.NonNullGuid, - Resolver = Resolve(x => x.Id.ToString()), - Description = "The id of the asset." - }); - - AddField(new FieldType - { - Name = "version", - ResolvedType = AllTypes.NonNullInt, - Resolver = Resolve(x => x.Version), - Description = "The version of the asset." - }); - - AddField(new FieldType - { - Name = "created", - ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.Created), - Description = "The date and time when the asset has been created." - }); - - AddField(new FieldType - { - Name = "createdBy", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.CreatedBy.ToString()), - Description = "The user that has created the asset." - }); - - AddField(new FieldType - { - Name = "lastModified", - ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.LastModified), - Description = "The date and time when the asset has been modified last." - }); - - AddField(new FieldType - { - Name = "lastModifiedBy", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.LastModifiedBy.ToString()), - Description = "The user that has updated the asset last." - }); - - AddField(new FieldType - { - Name = "mimeType", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.MimeType), - Description = "The mime type." - }); - - AddField(new FieldType - { - Name = "url", - ResolvedType = AllTypes.NonNullString, - Resolver = model.ResolveAssetUrl(), - Description = "The url to the asset." - }); - - AddField(new FieldType - { - Name = "thumbnailUrl", - ResolvedType = AllTypes.String, - Resolver = model.ResolveAssetThumbnailUrl(), - Description = "The thumbnail url to the asset." - }); - - AddField(new FieldType - { - Name = "fileName", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.FileName), - Description = "The file name." - }); - - AddField(new FieldType - { - Name = "fileHash", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.FileHash), - Description = "The hash of the file. Can be null for old files." - }); - - AddField(new FieldType - { - Name = "fileType", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.FileName.FileType()), - Description = "The file type." - }); - - AddField(new FieldType - { - Name = "fileSize", - ResolvedType = AllTypes.NonNullInt, - Resolver = Resolve(x => x.FileSize), - Description = "The size of the file in bytes." - }); - - AddField(new FieldType - { - Name = "fileVersion", - ResolvedType = AllTypes.NonNullInt, - Resolver = Resolve(x => x.FileVersion), - Description = "The version of the file." - }); - - AddField(new FieldType - { - Name = "slug", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.Slug), - Description = "The file name as slug." - }); - - AddField(new FieldType - { - Name = "isImage", - ResolvedType = AllTypes.NonNullBoolean, - Resolver = Resolve(x => x.IsImage), - Description = "Determines of the created file is an image." - }); - - AddField(new FieldType - { - Name = "pixelWidth", - ResolvedType = AllTypes.Int, - Resolver = Resolve(x => x.PixelWidth), - Description = "The width of the image in pixels if the asset is an image." - }); - - AddField(new FieldType - { - Name = "pixelHeight", - ResolvedType = AllTypes.Int, - Resolver = Resolve(x => x.PixelHeight), - Description = "The height of the image in pixels if the asset is an image." - }); - - AddField(new FieldType - { - Name = "tags", - ResolvedType = null, - Resolver = Resolve(x => x.TagNames), - Description = "The asset tags.", - Type = AllTypes.NonNullTagsType - }); - - if (model.CanGenerateAssetSourceUrl) - { - AddField(new FieldType - { - Name = "sourceUrl", - ResolvedType = AllTypes.NonNullString, - Resolver = model.ResolveAssetSourceUrl(), - Description = "The source url of the asset." - }); - } - - Description = "An asset"; - } - - private static IFieldResolver Resolve(Func action) - { - return new FuncFieldResolver(c => action(c.Source)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs deleted file mode 100644 index 517776b3f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs +++ /dev/null @@ -1,91 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types -{ - public sealed class ContentDataGraphType : ObjectGraphType - { - public ContentDataGraphType(ISchemaEntity schema, string schemaName, string schemaType, IGraphModel model) - { - Name = $"{schemaType}DataDto"; - - foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields()) - { - var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName); - - if (valueResolver != null) - { - var displayName = field.DisplayName(); - - var fieldGraphType = new ObjectGraphType - { - Name = $"{schemaType}Data{typeName}Dto" - }; - - var partition = model.ResolvePartition(field.Partitioning); - - foreach (var partitionItem in partition) - { - var key = partitionItem.Key; - - fieldGraphType.AddField(new FieldType - { - Name = key.EscapePartition(), - Resolver = PartitionResolver(valueResolver, key), - ResolvedType = resolvedType, - Description = field.RawProperties.Hints - }); - } - - fieldGraphType.Description = $"The structure of the {displayName} field of the {schemaName} content type."; - - AddField(new FieldType - { - Name = fieldName, - Resolver = FieldResolver(field), - ResolvedType = fieldGraphType, - Description = $"The {displayName} field." - }); - } - } - - Description = $"The structure of the {schemaName} content type."; - } - - private static FuncFieldResolver PartitionResolver(ValueResolver valueResolver, string key) - { - return new FuncFieldResolver(c => - { - if (((ContentFieldData)c.Source).TryGetValue(key, out var value)) - { - return valueResolver(value, c); - } - else - { - return null; - } - }); - } - - private static FuncFieldResolver> FieldResolver(RootField field) - { - return new FuncFieldResolver>(c => - { - return c.Source.GetOrDefault(field.Name); - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs deleted file mode 100644 index d07ee4b82..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs +++ /dev/null @@ -1,144 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Entities.Schemas; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types -{ - public sealed class ContentGraphType : ObjectGraphType - { - private readonly ISchemaEntity schema; - private readonly string schemaType; - private readonly string schemaName; - - public ContentGraphType(ISchemaEntity schema) - { - this.schema = schema; - - schemaType = schema.TypeName(); - schemaName = schema.DisplayName(); - - Name = $"{schemaType}"; - - AddField(new FieldType - { - Name = "id", - ResolvedType = AllTypes.NonNullGuid, - Resolver = Resolve(x => x.Id), - Description = $"The id of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "version", - ResolvedType = AllTypes.NonNullInt, - Resolver = Resolve(x => x.Version), - Description = $"The version of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "created", - ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.Created), - Description = $"The date and time when the {schemaName} content has been created." - }); - - AddField(new FieldType - { - Name = "createdBy", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.CreatedBy.ToString()), - Description = $"The user that has created the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "lastModified", - ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.LastModified), - Description = $"The date and time when the {schemaName} content has been modified last." - }); - - AddField(new FieldType - { - Name = "lastModifiedBy", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.LastModifiedBy.ToString()), - Description = $"The user that has updated the {schemaName} content last." - }); - - AddField(new FieldType - { - Name = "status", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.Status.Name.ToUpperInvariant()), - Description = $"The the status of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "statusColor", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.StatusColor), - Description = $"The color status of the {schemaName} content." - }); - - Interface(); - - Description = $"The structure of a {schemaName} content type."; - - IsTypeOf = CheckType; - } - - private bool CheckType(object value) - { - return value is IContentEntity content && content.SchemaId?.Id == schema.Id; - } - - public void Initialize(IGraphModel model) - { - AddField(new FieldType - { - Name = "url", - ResolvedType = AllTypes.NonNullString, - Resolver = model.ResolveContentUrl(schema), - Description = $"The url to the the {schemaName} content." - }); - - var contentDataType = new ContentDataGraphType(schema, schemaName, schemaType, model); - - if (contentDataType.Fields.Any()) - { - AddField(new FieldType - { - Name = "data", - ResolvedType = new NonNullGraphType(contentDataType), - Resolver = Resolve(x => x.Data), - Description = $"The data of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "dataDraft", - ResolvedType = contentDataType, - Resolver = Resolve(x => x.DataDraft), - Description = $"The draft data of the {schemaName} content." - }); - } - } - - private static IFieldResolver Resolve(Func action) - { - return new FuncFieldResolver(c => action(c.Source)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs deleted file mode 100644 index 523b58032..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using GraphQL.Types; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types -{ - public sealed class ContentUnionGraphType : UnionGraphType - { - private readonly Dictionary types = new Dictionary(); - - public ContentUnionGraphType(string fieldName, Dictionary schemaTypes, IEnumerable schemaIds) - { - Name = $"{fieldName}ReferenceUnionDto"; - - if (schemaIds?.Any() == true) - { - foreach (var schemaId in schemaIds) - { - var schemaType = schemaTypes.GetOrDefault(schemaId); - - if (schemaType != null) - { - types[schemaId] = schemaType; - } - } - } - else - { - foreach (var schemaType in schemaTypes) - { - types[schemaType.Key] = schemaType.Value; - } - } - - foreach (var type in types) - { - AddPossibleType(type.Value); - } - - ResolveType = value => - { - if (value is IContentEntity content) - { - return types.GetOrDefault(content.SchemaId.Id); - } - - return null; - }; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs deleted file mode 100644 index cd23927b6..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types -{ - public sealed class NestedGraphType : ObjectGraphType - { - public NestedGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field, string fieldName) - { - var schemaType = schema.TypeName(); - var schemaName = schema.DisplayName(); - - var fieldDisplayName = field.DisplayName(); - - Name = $"{schemaType}{fieldName}ChildDto"; - - foreach (var (nestedField, nestedName, _) in field.Fields.SafeFields()) - { - var fieldInfo = model.GetGraphType(schema, nestedField, nestedName); - - if (fieldInfo.ResolveType != null) - { - var resolver = ValueResolver(nestedField, fieldInfo); - - AddField(new FieldType - { - Name = nestedName, - Resolver = resolver, - ResolvedType = fieldInfo.ResolveType, - Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field." - }); - } - } - - Description = $"The structure of the {schemaName}.{fieldDisplayName} nested schema."; - } - - private static FuncFieldResolver ValueResolver(NestedField nestedField, (IGraphType ResolveType, ValueResolver Resolver) fieldInfo) - { - return new FuncFieldResolver(c => - { - if (((JsonObject)c.Source).TryGetValue(nestedField.Name, out var value)) - { - return fieldInfo.Resolver(value, c); - } - else - { - return fieldInfo; - } - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs deleted file mode 100644 index e038b0432..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs +++ /dev/null @@ -1,150 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using GraphQL.Types; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types -{ - public delegate object ValueResolver(IJsonValue value, ResolveFieldContext context); - - public sealed class QueryGraphTypeVisitor : IFieldVisitor<(IGraphType ResolveType, ValueResolver Resolver)> - { - private static readonly ValueResolver NoopResolver = (value, c) => value; - private readonly Dictionary schemaTypes; - private readonly ISchemaEntity schema; - private readonly IGraphModel model; - private readonly IGraphType assetListType; - private readonly string fieldName; - - public QueryGraphTypeVisitor(ISchemaEntity schema, - Dictionary schemaTypes, - IGraphModel model, - IGraphType assetListType, - string fieldName) - { - this.model = model; - this.assetListType = assetListType; - this.schema = schema; - this.schemaTypes = schemaTypes; - this.fieldName = fieldName; - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IArrayField field) - { - return ResolveNested(field); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveAssets(); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopBoolean); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopDate); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopGeolocation); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopJson); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopFloat); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveReferences(field); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopString); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopTags); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return (null, null); - } - - private static (IGraphType ResolveType, ValueResolver Resolver) ResolveDefault(IGraphType type) - { - return (type, NoopResolver); - } - - private (IGraphType ResolveType, ValueResolver Resolver) ResolveNested(IArrayField field) - { - var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedGraphType(model, schema, field, fieldName))); - - return (schemaFieldType, NoopResolver); - } - - private (IGraphType ResolveType, ValueResolver Resolver) ResolveAssets() - { - var resolver = new ValueResolver((value, c) => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.GetReferencedAssetsAsync(value); - }); - - return (assetListType, resolver); - } - - private (IGraphType ResolveType, ValueResolver Resolver) ResolveReferences(IField field) - { - IGraphType contentType = schemaTypes.GetOrDefault(field.Properties.SingleId()); - - if (contentType == null) - { - var union = new ContentUnionGraphType(fieldName, schemaTypes, field.Properties.SchemaIds); - - if (!union.PossibleTypes.Any()) - { - return (null, null); - } - - contentType = union; - } - - var resolver = new ValueResolver((value, c) => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.GetReferencedContentsAsync(value); - }); - - var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType)); - - return (schemaFieldType, resolver); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs deleted file mode 100644 index 1245e857f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using GraphQL.Language.AST; -using GraphQL.Types; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils -{ - public sealed class GuidGraphType2 : ScalarGraphType - { - public GuidGraphType2() - { - Name = "Guid"; - - Description = "The `Guid` scalar type global unique identifier"; - } - - public override object Serialize(object value) - { - return ParseValue(value)?.ToString(); - } - - public override object ParseValue(object value) - { - if (value is Guid guid) - { - return guid; - } - - var inputValue = value?.ToString().Trim('"'); - - if (Guid.TryParse(inputValue, out guid)) - { - return guid; - } - - return null; - } - - public override object ParseLiteral(IValue value) - { - if (value is StringValue stringValue) - { - return ParseValue(stringValue.Value); - } - - return null; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs deleted file mode 100644 index 2f45fe6a9..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GraphQL.Language.AST; -using GraphQL.Types; -using NodaTime.Text; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils -{ - public sealed class InstantGraphType : DateGraphType - { - public override object Serialize(object value) - { - return ParseValue(value); - } - - public override object ParseValue(object value) - { - return InstantPattern.General.Parse(value.ToString()).Value; - } - - public override object ParseLiteral(IValue value) - { - if (value is InstantValue timeValue) - { - return ParseValue(timeValue.Value); - } - - if (value is StringValue stringValue) - { - return ParseValue(stringValue.Value); - } - - return null; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs deleted file mode 100644 index a433a5fb3..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GraphQL.Language.AST; -using GraphQL.Types; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils -{ - public sealed class JsonConverter : IAstFromValueConverter - { - public static readonly JsonConverter Instance = new JsonConverter(); - - private JsonConverter() - { - } - - public IValue Convert(object value, IGraphType type) - { - return new JsonValue(value as JsonObject); - } - - public bool Matches(object value, IGraphType type) - { - return value is JsonObject; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs deleted file mode 100644 index 3b99c8412..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GraphQL.Language.AST; -using GraphQL.Types; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils -{ - public sealed class JsonGraphType : ScalarGraphType - { - public JsonGraphType() - { - Name = "Json"; - - Description = "Unstructured Json object"; - } - - public override object Serialize(object value) - { - return value; - } - - public override object ParseValue(object value) - { - return value; - } - - public override object ParseLiteral(IValue value) - { - if (value is JsonValue jsonGraphType) - { - return jsonGraphType.Value; - } - - return value; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValue.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValue.cs deleted file mode 100644 index 01449380f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValue.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GraphQL.Language.AST; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils -{ - public sealed class JsonValue : ValueNode - { - public JsonValue(IJsonValue value) - { - Value = value; - } - - protected override bool Equals(ValueNode node) - { - return false; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs deleted file mode 100644 index e7c19aac1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs +++ /dev/null @@ -1,136 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Contents.Guards -{ - public static class GuardContent - { - public static async Task CanCreate(ISchemaEntity schema, IContentWorkflow contentWorkflow, CreateContent command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot created content.", e => - { - ValidateData(command, e); - }); - - if (schema.SchemaDef.IsSingleton && command.ContentId != schema.Id) - { - throw new DomainException("Singleton content cannot be created."); - } - - if (command.Publish && !await contentWorkflow.CanPublishOnCreateAsync(schema, command.Data, command.User)) - { - throw new DomainException("Content workflow prevents publishing."); - } - } - - public static async Task CanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, UpdateContent command, bool isProposal) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot update content.", e => - { - ValidateData(command, e); - }); - - if (!isProposal) - { - await ValidateCanUpdate(content, contentWorkflow); - } - } - - public static async Task CanPatch(IContentEntity content, IContentWorkflow contentWorkflow, PatchContent command, bool isProposal) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot patch content.", e => - { - ValidateData(command, e); - }); - - if (!isProposal) - { - await ValidateCanUpdate(content, contentWorkflow); - } - } - - public static void CanDiscardChanges(bool isPending, DiscardChanges command) - { - Guard.NotNull(command, nameof(command)); - - if (!isPending) - { - throw new DomainException("The content has no pending changes."); - } - } - - public static Task CanChangeStatus(ISchemaEntity schema, IContentEntity content, IContentWorkflow contentWorkflow, ChangeContentStatus command, bool isChangeConfirm) - { - Guard.NotNull(command, nameof(command)); - - if (schema.SchemaDef.IsSingleton && command.Status != Status.Published) - { - throw new DomainException("Singleton content cannot be changed."); - } - - return Validate.It(() => "Cannot change status.", async e => - { - if (isChangeConfirm) - { - if (!content.IsPending) - { - e("Content has no changes to publish.", nameof(command.Status)); - } - } - else if (!await contentWorkflow.CanMoveToAsync(content, command.Status, command.User)) - { - e($"Cannot change status from {content.Status} to {command.Status}.", nameof(command.Status)); - } - - if (command.DueTime.HasValue && command.DueTime.Value < SystemClock.Instance.GetCurrentInstant()) - { - e("Due time must be in the future.", nameof(command.DueTime)); - } - }); - } - - public static void CanDelete(ISchemaEntity schema, DeleteContent command) - { - Guard.NotNull(command, nameof(command)); - - if (schema.SchemaDef.IsSingleton) - { - throw new DomainException("Singleton content cannot be deleted."); - } - } - - private static void ValidateData(ContentDataCommand command, AddValidation e) - { - if (command.Data == null) - { - e(Not.Defined("Data"), nameof(command.Data)); - } - } - - private static async Task ValidateCanUpdate(IContentEntity content, IContentWorkflow contentWorkflow) - { - if (!await contentWorkflow.CanUpdateAsync(content)) - { - throw new DomainException($"The workflow does not allow updates at status {content.Status}"); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs deleted file mode 100644 index 1a7e53424..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public interface IContentEntity : - IEntity, - IEntityWithCreatedBy, - IEntityWithLastModifiedBy, - IEntityWithVersion - { - NamedId AppId { get; } - - NamedId SchemaId { get; } - - Status Status { get; } - - ScheduleJob ScheduleJob { get; } - - NamedContentData Data { get; } - - NamedContentData DataDraft { get; } - - bool IsPending { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs deleted file mode 100644 index b8e45f2eb..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public interface IEnrichedContentEntity : IContentEntity, IEntityWithCacheDependencies - { - bool CanUpdate { get; } - - string StatusColor { get; } - - string SchemaName { get; } - - string SchemaDisplayName { get; } - - RootField[] ReferenceFields { get; } - - StatusInfo[] Nexts { get; } - - NamedContentData ReferenceData { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs deleted file mode 100644 index b24333d95..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs +++ /dev/null @@ -1,377 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Core.ExtractReferenceIds; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public sealed class ContentEnricher : IContentEnricher - { - private const string DefaultColor = StatusColors.Draft; - private static readonly ILookup EmptyContents = Enumerable.Empty().ToLookup(x => x.Id); - private static readonly ILookup EmptyAssets = Enumerable.Empty().ToLookup(x => x.Id); - private readonly IAssetQueryService assetQuery; - private readonly IAssetUrlGenerator assetUrlGenerator; - private readonly Lazy contentQuery; - private readonly IContentWorkflow contentWorkflow; - - private IContentQueryService ContentQuery - { - get { return contentQuery.Value; } - } - - public ContentEnricher(IAssetQueryService assetQuery, IAssetUrlGenerator assetUrlGenerator, Lazy contentQuery, IContentWorkflow contentWorkflow) - { - Guard.NotNull(assetQuery, nameof(assetQuery)); - Guard.NotNull(assetUrlGenerator, nameof(assetUrlGenerator)); - Guard.NotNull(contentQuery, nameof(contentQuery)); - Guard.NotNull(contentWorkflow, nameof(contentWorkflow)); - - this.assetQuery = assetQuery; - this.assetUrlGenerator = assetUrlGenerator; - this.contentQuery = contentQuery; - this.contentWorkflow = contentWorkflow; - } - - public async Task EnrichAsync(IContentEntity content, Context context) - { - Guard.NotNull(content, nameof(content)); - - var enriched = await EnrichAsync(Enumerable.Repeat(content, 1), context); - - return enriched[0]; - } - - public async Task> EnrichAsync(IEnumerable contents, Context context) - { - Guard.NotNull(contents, nameof(contents)); - Guard.NotNull(context, nameof(context)); - - using (Profiler.TraceMethod()) - { - var results = new List(); - - if (contents.Any()) - { - var appVersion = context.App.Version; - - var cache = new Dictionary<(Guid, Status), StatusInfo>(); - - foreach (var content in contents) - { - var result = SimpleMapper.Map(content, new ContentEntity()); - - await EnrichColorAsync(content, result, cache); - - if (ShouldEnrichWithStatuses(context)) - { - await EnrichNextsAsync(content, result, context); - await EnrichCanUpdateAsync(content, result); - } - - results.Add(result); - } - - foreach (var group in results.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - - foreach (var content in group) - { - content.CacheDependencies = new HashSet - { - schema.Id, - schema.Version - }; - } - - if (ShouldEnrichWithSchema(context)) - { - var referenceFields = schema.SchemaDef.ReferenceFields().ToArray(); - - var schemaName = schema.SchemaDef.Name; - var schemaDisplayName = schema.SchemaDef.DisplayNameUnchanged(); - - foreach (var content in group) - { - content.ReferenceFields = referenceFields; - content.SchemaName = schemaName; - content.SchemaDisplayName = schemaDisplayName; - } - } - } - - if (ShouldEnrich(context)) - { - await EnrichReferencesAsync(context, results); - await EnrichAssetsAsync(context, results); - } - } - - return results; - } - } - - private async Task EnrichAssetsAsync(Context context, List contents) - { - var ids = new HashSet(); - - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - - AddAssetIds(ids, schema, group); - } - - var assets = await GetAssetsAsync(context, ids); - - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - - ResolveAssets(schema, group, assets); - } - } - - private async Task EnrichReferencesAsync(Context context, List contents) - { - var ids = new HashSet(); - - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - - AddReferenceIds(ids, schema, group); - } - - var references = await GetReferencesAsync(context, ids); - - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - - await ResolveReferencesAsync(context, schema, group, references); - } - } - - private async Task ResolveReferencesAsync(Context context, ISchemaEntity schema, IEnumerable contents, ILookup references) - { - var formatted = new Dictionary(); - - foreach (var field in schema.SchemaDef.ResolvingReferences()) - { - foreach (var content in contents) - { - if (content.ReferenceData == null) - { - content.ReferenceData = new NamedContentData(); - } - - var fieldReference = content.ReferenceData.GetOrAddNew(field.Name); - - try - { - if (content.DataDraft.TryGetValue(field.Name, out var fieldData)) - { - foreach (var partitionValue in fieldData) - { - var referencedContents = - field.GetReferencedIds(partitionValue.Value, Ids.ContentOnly) - .Select(x => references[x]) - .SelectMany(x => x) - .ToList(); - - if (referencedContents.Count == 1) - { - var reference = referencedContents[0]; - - var referencedSchema = await ContentQuery.GetSchemaOrThrowAsync(context, reference.SchemaId.Id.ToString()); - - content.CacheDependencies.Add(referencedSchema.Id); - content.CacheDependencies.Add(referencedSchema.Version); - content.CacheDependencies.Add(reference.Id); - content.CacheDependencies.Add(reference.Version); - - var value = formatted.GetOrAdd(reference, x => Format(x, context, referencedSchema)); - - fieldReference.AddJsonValue(partitionValue.Key, value); - } - else if (referencedContents.Count > 1) - { - var value = CreateFallback(context, referencedContents); - - fieldReference.AddJsonValue(partitionValue.Key, value); - } - } - } - } - catch (DomainObjectNotFoundException) - { - continue; - } - } - } - } - - private void ResolveAssets(ISchemaEntity schema, IGrouping contents, ILookup assets) - { - foreach (var field in schema.SchemaDef.ResolvingAssets()) - { - foreach (var content in contents) - { - if (content.ReferenceData == null) - { - content.ReferenceData = new NamedContentData(); - } - - var fieldReference = content.ReferenceData.GetOrAddNew(field.Name); - - if (content.DataDraft.TryGetValue(field.Name, out var fieldData)) - { - foreach (var partitionValue in fieldData) - { - var referencedImage = - field.GetReferencedIds(partitionValue.Value, Ids.ContentOnly) - .Select(x => assets[x]) - .SelectMany(x => x) - .FirstOrDefault(x => x.IsImage); - - if (referencedImage != null) - { - var url = assetUrlGenerator.GenerateUrl(referencedImage.Id.ToString()); - - content.CacheDependencies.Add(referencedImage.Id); - content.CacheDependencies.Add(referencedImage.Version); - - fieldReference.AddJsonValue(partitionValue.Key, JsonValue.Create(url)); - } - } - } - } - } - } - - private static JsonObject Format(IContentEntity content, Context context, ISchemaEntity referencedSchema) - { - return content.DataDraft.FormatReferences(referencedSchema.SchemaDef, context.App.LanguagesConfig); - } - - private static JsonObject CreateFallback(Context context, List referencedContents) - { - var text = $"{referencedContents.Count} Reference(s)"; - - var value = JsonValue.Object(); - - foreach (var language in context.App.LanguagesConfig) - { - value.Add(language.Key, text); - } - - return value; - } - - private void AddReferenceIds(HashSet ids, ISchemaEntity schema, IEnumerable contents) - { - foreach (var content in contents) - { - ids.AddRange(content.DataDraft.GetReferencedIds(schema.SchemaDef.ResolvingReferences(), Ids.ContentOnly)); - } - } - - private void AddAssetIds(HashSet ids, ISchemaEntity schema, IEnumerable contents) - { - foreach (var content in contents) - { - ids.AddRange(content.DataDraft.GetReferencedIds(schema.SchemaDef.ResolvingAssets(), Ids.ContentOnly)); - } - } - - private async Task> GetReferencesAsync(Context context, HashSet ids) - { - if (ids.Count == 0) - { - return EmptyContents; - } - - var references = await ContentQuery.QueryAsync(context.Clone().WithNoContentEnrichment(true), ids.ToList()); - - return references.ToLookup(x => x.Id); - } - - private async Task> GetAssetsAsync(Context context, HashSet ids) - { - if (ids.Count == 0) - { - return EmptyAssets; - } - - var assets = await assetQuery.QueryAsync(context.Clone().WithNoAssetEnrichment(true), Q.Empty.WithIds(ids)); - - return assets.ToLookup(x => x.Id); - } - - private async Task EnrichCanUpdateAsync(IContentEntity content, ContentEntity result) - { - result.CanUpdate = await contentWorkflow.CanUpdateAsync(content); - } - - private async Task EnrichNextsAsync(IContentEntity content, ContentEntity result, Context context) - { - result.Nexts = await contentWorkflow.GetNextsAsync(content, context.User); - } - - private async Task EnrichColorAsync(IContentEntity content, ContentEntity result, Dictionary<(Guid, Status), StatusInfo> cache) - { - result.StatusColor = await GetColorAsync(content, cache); - } - - private async Task GetColorAsync(IContentEntity content, Dictionary<(Guid, Status), StatusInfo> cache) - { - if (!cache.TryGetValue((content.SchemaId.Id, content.Status), out var info)) - { - info = await contentWorkflow.GetInfoAsync(content); - - if (info == null) - { - info = new StatusInfo(content.Status, DefaultColor); - } - - cache[(content.SchemaId.Id, content.Status)] = info; - } - - return info.Color; - } - - private static bool ShouldEnrichWithSchema(Context context) - { - return context.IsFrontendClient; - } - - private static bool ShouldEnrichWithStatuses(Context context) - { - return context.IsFrontendClient || context.IsResolveFlow(); - } - - private static bool ShouldEnrich(Context context) - { - return context.IsFrontendClient && !context.IsNoEnrichment(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs deleted file mode 100644 index 5abb7f687..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public sealed class ContentLoader : IContentLoader - { - private readonly IGrainFactory grainFactory; - - public ContentLoader(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public async Task GetAsync(Guid id, long version) - { - using (Profiler.TraceMethod()) - { - var grain = grainFactory.GetGrain(id); - - var content = await grain.GetStateAsync(version); - - if (content.Value == null || (version > EtagVersion.Any && content.Value.Version != version)) - { - throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity)); - } - - return content.Value; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs deleted file mode 100644 index 6de9304c2..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs +++ /dev/null @@ -1,205 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using Microsoft.OData; -using Microsoft.OData.Edm; -using NJsonSchema; -using Squidex.Domain.Apps.Core.GenerateEdmSchema; -using Squidex.Domain.Apps.Core.GenerateJsonSchema; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Caching; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.Queries.OData; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public class ContentQueryParser : CachingProviderBase - { - private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); - private readonly IJsonSerializer jsonSerializer; - private readonly ContentOptions options; - - public ContentQueryParser(IMemoryCache cache, IJsonSerializer jsonSerializer, IOptions options) - : base(cache) - { - this.jsonSerializer = jsonSerializer; - this.options = options.Value; - } - - public virtual ClrQuery ParseQuery(Context context, ISchemaEntity schema, Q q) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(schema, nameof(schema)); - - using (Profiler.TraceMethod()) - { - var result = new ClrQuery(); - - if (!string.IsNullOrWhiteSpace(q?.JsonQuery)) - { - result = ParseJson(context, schema, q.JsonQuery); - } - else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) - { - result = ParseOData(context, schema, q.ODataQuery); - } - - if (result.Sort.Count == 0) - { - result.Sort.Add(new SortNode(new List { "lastModified" }, SortOrder.Descending)); - } - - if (result.Take == long.MaxValue) - { - result.Take = options.DefaultPageSize; - } - else if (result.Take > options.MaxResults) - { - result.Take = options.MaxResults; - } - - return result; - } - } - - private ClrQuery ParseJson(Context context, ISchemaEntity schema, string json) - { - var jsonSchema = BuildJsonSchema(context, schema); - - return jsonSchema.Parse(json, jsonSerializer); - } - - private ClrQuery ParseOData(Context context, ISchemaEntity schema, string odata) - { - try - { - var model = BuildEdmModel(context, schema); - - return model.ParseQuery(odata).ToQuery(); - } - catch (NotSupportedException) - { - throw new ValidationException("OData operation is not supported."); - } - catch (ODataException ex) - { - throw new ValidationException($"Failed to parse query: {ex.Message}", ex); - } - } - - private JsonSchema BuildJsonSchema(Context context, ISchemaEntity schema) - { - var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient); - - var result = Cache.GetOrCreate(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheTime; - - return BuildJsonSchema(schema.SchemaDef, context.App, context.IsFrontendClient); - }); - - return result; - } - - private IEdmModel BuildEdmModel(Context context, ISchemaEntity schema) - { - var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient); - - var result = Cache.GetOrCreate(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheTime; - - return BuildEdmModel(schema.SchemaDef, context.App, context.IsFrontendClient); - }); - - return result; - } - - private static JsonSchema BuildJsonSchema(Schema schema, IAppEntity app, bool withHiddenFields) - { - var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), (n, s) => s, withHiddenFields); - - return new ContentSchemaBuilder().CreateContentSchema(schema, dataSchema); - } - - private static EdmModel BuildEdmModel(Schema schema, IAppEntity app, bool withHiddenFields) - { - var model = new EdmModel(); - - var pascalAppName = app.Name.ToPascalCase(); - var pascalSchemaName = schema.Name.ToPascalCase(); - - var typeFactory = new EdmTypeFactory(name => - { - var finalName = pascalSchemaName; - - if (!string.IsNullOrWhiteSpace(name)) - { - finalName += "."; - finalName += name; - } - - var result = model.SchemaElements.OfType().FirstOrDefault(x => x.Name == finalName); - - if (result != null) - { - return (result, false); - } - - result = new EdmComplexType(pascalAppName, finalName); - - model.AddElement(result); - - return (result, true); - }); - - var schemaType = schema.BuildEdmType(withHiddenFields, app.PartitionResolver(), typeFactory); - - var entityType = new EdmEntityType(app.Name.ToPascalCase(), schema.Name); - entityType.AddStructuralProperty(nameof(IContentEntity.Id).ToCamelCase(), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(IContentEntity.Created).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); - entityType.AddStructuralProperty(nameof(IContentEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(IContentEntity.LastModified).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); - entityType.AddStructuralProperty(nameof(IContentEntity.LastModifiedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(IContentEntity.Status).ToCamelCase(), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(IContentEntity.Version).ToCamelCase(), EdmPrimitiveTypeKind.Int32); - entityType.AddStructuralProperty(nameof(IContentEntity.Data).ToCamelCase(), new EdmComplexTypeReference(schemaType, false)); - - var container = new EdmEntityContainer("Squidex", "Container"); - - container.AddEntitySet("ContentSet", entityType); - - model.AddElement(container); - model.AddElement(schemaType); - model.AddElement(entityType); - - return model; - } - - private static string BuildEmdCacheKey(IAppEntity app, ISchemaEntity schema, bool withHidden) - { - return $"EDM/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; - } - - private static string BuildJsonCacheKey(IAppEntity app, ISchemaEntity schema, bool withHidden) - { - return $"JSON/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs deleted file mode 100644 index aa43adaa1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ /dev/null @@ -1,341 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Reflection; -using Squidex.Shared; - -#pragma warning disable RECS0147 - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public sealed class ContentQueryService : IContentQueryService - { - private static readonly Status[] StatusPublishedOnly = { Status.Published }; - private static readonly IResultList EmptyContents = ResultList.CreateFrom(0); - private readonly IAppProvider appProvider; - private readonly IAssetUrlGenerator assetUrlGenerator; - private readonly IContentEnricher contentEnricher; - private readonly IContentRepository contentRepository; - private readonly IContentLoader contentVersionLoader; - private readonly IScriptEngine scriptEngine; - private readonly ContentQueryParser queryParser; - - public ContentQueryService( - IAppProvider appProvider, - IAssetUrlGenerator assetUrlGenerator, - IContentEnricher contentEnricher, - IContentRepository contentRepository, - IContentLoader contentVersionLoader, - IScriptEngine scriptEngine, - ContentQueryParser queryParser) - { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(assetUrlGenerator, nameof(assetUrlGenerator)); - Guard.NotNull(contentEnricher, nameof(contentEnricher)); - Guard.NotNull(contentRepository, nameof(contentRepository)); - Guard.NotNull(contentVersionLoader, nameof(contentVersionLoader)); - Guard.NotNull(queryParser, nameof(queryParser)); - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - - this.appProvider = appProvider; - this.assetUrlGenerator = assetUrlGenerator; - this.contentEnricher = contentEnricher; - this.contentRepository = contentRepository; - this.contentVersionLoader = contentVersionLoader; - this.queryParser = queryParser; - this.scriptEngine = scriptEngine; - this.queryParser = queryParser; - } - - public async Task FindContentAsync(Context context, string schemaIdOrName, Guid id, long version = -1) - { - Guard.NotNull(context, nameof(context)); - - var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); - - CheckPermission(context, schema); - - using (Profiler.TraceMethod()) - { - IContentEntity content; - - if (version > EtagVersion.Empty) - { - content = await FindByVersionAsync(id, version); - } - else - { - content = await FindCoreAsync(context, id, schema); - } - - if (content == null || content.SchemaId.Id != schema.Id) - { - throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity)); - } - - return await TransformAsync(context, schema, content); - } - } - - public async Task> QueryAsync(Context context, string schemaIdOrName, Q query) - { - Guard.NotNull(context, nameof(context)); - - var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); - - CheckPermission(context, schema); - - using (Profiler.TraceMethod()) - { - IResultList contents; - - if (query.Ids != null && query.Ids.Count > 0) - { - contents = await QueryByIdsAsync(context, schema, query); - } - else - { - contents = await QueryByQueryAsync(context, schema, query); - } - - return await TransformAsync(context, schema, contents); - } - } - - public async Task> QueryAsync(Context context, IReadOnlyList ids) - { - Guard.NotNull(context, nameof(context)); - - using (Profiler.TraceMethod()) - { - if (ids == null || ids.Count == 0) - { - return EmptyContents; - } - - var results = new List(); - - var contents = await QueryCoreAsync(context, ids); - - foreach (var group in contents.GroupBy(x => x.Schema.Id)) - { - var schema = group.First().Schema; - - if (HasPermission(context, schema)) - { - var enriched = await TransformCoreAsync(context, schema, group.Select(x => x.Content)); - - results.AddRange(enriched); - } - } - - return ResultList.Create(results.Count, results.SortList(x => x.Id, ids)); - } - } - - private async Task> TransformAsync(Context context, ISchemaEntity schema, IResultList contents) - { - var transformed = await TransformCoreAsync(context, schema, contents); - - return ResultList.Create(contents.Total, transformed); - } - - private async Task TransformAsync(Context context, ISchemaEntity schema, IContentEntity content) - { - var transformed = await TransformCoreAsync(context, schema, Enumerable.Repeat(content, 1)); - - return transformed[0]; - } - - private async Task> TransformCoreAsync(Context context, ISchemaEntity schema, IEnumerable contents) - { - using (Profiler.TraceMethod()) - { - var results = new List(); - - var converters = GenerateConverters(context).ToArray(); - - var scriptText = schema.SchemaDef.Scripts.Query; - var scripting = !string.IsNullOrWhiteSpace(scriptText); - - var enriched = await contentEnricher.EnrichAsync(contents, context); - - foreach (var content in enriched) - { - var result = SimpleMapper.Map(content, new ContentEntity()); - - if (result.Data != null) - { - if (!context.IsFrontendClient && scripting) - { - var ctx = new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id }; - - result.Data = scriptEngine.Transform(ctx, scriptText); - } - - result.Data = result.Data.ConvertName2Name(schema.SchemaDef, converters); - } - - if (result.DataDraft != null && (context.IsUnpublished() || context.IsFrontendClient)) - { - result.DataDraft = result.DataDraft.ConvertName2Name(schema.SchemaDef, converters); - } - else - { - result.DataDraft = null; - } - - results.Add(result); - } - - return results; - } - } - - private IEnumerable GenerateConverters(Context context) - { - if (!context.IsFrontendClient) - { - yield return FieldConverters.ExcludeHidden(); - yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden()); - } - - yield return FieldConverters.ExcludeChangedTypes(); - yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes()); - - yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig); - yield return FieldConverters.ResolveLanguages(context.App.LanguagesConfig); - - if (!context.IsFrontendClient) - { - yield return FieldConverters.ResolveFallbackLanguages(context.App.LanguagesConfig); - - var languages = context.Languages(); - - if (languages.Any()) - { - yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, languages); - } - - var assetUrls = context.AssetUrls(); - - if (assetUrls.Any()) - { - yield return FieldConverters.ResolveAssetUrls(assetUrls.ToList(), assetUrlGenerator); - } - } - } - - public async Task GetSchemaOrThrowAsync(Context context, string schemaIdOrName) - { - ISchemaEntity schema = null; - - if (Guid.TryParse(schemaIdOrName, out var id)) - { - schema = await appProvider.GetSchemaAsync(context.App.Id, id); - } - - if (schema == null) - { - schema = await appProvider.GetSchemaAsync(context.App.Id, schemaIdOrName); - } - - if (schema == null) - { - throw new DomainObjectNotFoundException(schemaIdOrName, typeof(ISchemaEntity)); - } - - return schema; - } - - private static void CheckPermission(Context context, params ISchemaEntity[] schemas) - { - foreach (var schema in schemas) - { - if (!HasPermission(context, schema)) - { - throw new DomainForbiddenException("You do not have permission for this schema."); - } - } - } - - private static bool HasPermission(Context context, ISchemaEntity schema) - { - var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name); - - return context.Permissions.Allows(permission); - } - - private static Status[] GetStatus(Context context) - { - if (context.IsFrontendClient || context.IsUnpublished()) - { - return null; - } - else - { - return StatusPublishedOnly; - } - } - - private async Task> QueryByQueryAsync(Context context, ISchemaEntity schema, Q query) - { - var parsedQuery = queryParser.ParseQuery(context, schema, query); - - return await QueryCoreAsync(context, schema, parsedQuery); - } - - private async Task> QueryByIdsAsync(Context context, ISchemaEntity schema, Q query) - { - var contents = await QueryCoreAsync(context, schema, query.Ids.ToHashSet()); - - return contents.SortSet(x => x.Id, query.Ids); - } - - private Task> QueryCoreAsync(Context context, IReadOnlyList ids) - { - return contentRepository.QueryAsync(context.App, GetStatus(context), new HashSet(ids), WithDraft(context)); - } - - private Task> QueryCoreAsync(Context context, ISchemaEntity schema, ClrQuery query) - { - return contentRepository.QueryAsync(context.App, schema, GetStatus(context), context.IsFrontendClient, query, WithDraft(context)); - } - - private Task> QueryCoreAsync(Context context, ISchemaEntity schema, HashSet ids) - { - return contentRepository.QueryAsync(context.App, schema, GetStatus(context), ids, WithDraft(context)); - } - - private Task FindCoreAsync(Context context, Guid id, ISchemaEntity schema) - { - return contentRepository.FindContentAsync(context.App, schema, GetStatus(context), id, WithDraft(context)); - } - - private Task FindByVersionAsync(Guid id, long version) - { - return contentVersionLoader.GetAsync(id, version); - } - - private static bool WithDraft(Context context) - { - return context.IsUnpublished() || context.IsFrontendClient; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs deleted file mode 100644 index e0f812403..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs +++ /dev/null @@ -1,71 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public sealed class FilterTagTransformer : TransformVisitor - { - private readonly ITagService tagService; - private readonly ISchemaEntity schema; - private readonly Guid appId; - - private FilterTagTransformer(Guid appId, ISchemaEntity schema, ITagService tagService) - { - this.appId = appId; - this.schema = schema; - this.tagService = tagService; - } - - public static FilterNode Transform(FilterNode nodeIn, Guid appId, ISchemaEntity schema, ITagService tagService) - { - Guard.NotNull(nodeIn, nameof(nodeIn)); - Guard.NotNull(tagService, nameof(tagService)); - Guard.NotNull(schema, nameof(schema)); - - return nodeIn.Accept(new FilterTagTransformer(appId, schema, tagService)); - } - - public override FilterNode Visit(CompareFilter nodeIn) - { - if (nodeIn.Value.Value is string stringValue && IsDataPath(nodeIn.Path) && IsTagField(nodeIn.Path)) - { - var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Schemas(schema.Id), HashSet.Of(stringValue))).Result; - - if (tagNames.TryGetValue(stringValue, out var normalized)) - { - return new CompareFilter(nodeIn.Path, nodeIn.Operator, normalized); - } - } - - return nodeIn; - } - - private static bool IsDataPath(IReadOnlyList path) - { - return path.Count == 3 && string.Equals(path[0], nameof(IContentEntity.Data), StringComparison.OrdinalIgnoreCase); - } - - private bool IsTagField(IReadOnlyList path) - { - return schema.SchemaDef.FieldsByName.TryGetValue(path[1], out var field) && IsTagField(field); - } - - private bool IsTagField(IField field) - { - return field is IField tags && tags.Properties.Normalization == TagsFieldNormalization.Schema; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs deleted file mode 100644 index d6a8e6456..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ /dev/null @@ -1,133 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public class QueryExecutionContext - { - private readonly ConcurrentDictionary cachedContents = new ConcurrentDictionary(); - private readonly ConcurrentDictionary cachedAssets = new ConcurrentDictionary(); - private readonly IContentQueryService contentQuery; - private readonly IAssetQueryService assetQuery; - private readonly Context context; - - public QueryExecutionContext(Context context, IAssetQueryService assetQuery, IContentQueryService contentQuery) - { - Guard.NotNull(assetQuery, nameof(assetQuery)); - Guard.NotNull(contentQuery, nameof(contentQuery)); - Guard.NotNull(context, nameof(context)); - - this.assetQuery = assetQuery; - this.contentQuery = contentQuery; - this.context = context; - } - - public virtual async Task FindAssetAsync(Guid id) - { - var asset = cachedAssets.GetOrDefault(id); - - if (asset == null) - { - asset = await assetQuery.FindAssetAsync(context, id); - - if (asset != null) - { - cachedAssets[asset.Id] = asset; - } - } - - return asset; - } - - public virtual async Task FindContentAsync(Guid schemaId, Guid id) - { - var content = cachedContents.GetOrDefault(id); - - if (content == null) - { - content = await contentQuery.FindContentAsync(context, schemaId.ToString(), id); - - if (content != null) - { - cachedContents[content.Id] = content; - } - } - - return content; - } - - public virtual async Task> QueryAssetsAsync(string query) - { - var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(query)); - - foreach (var asset in assets) - { - cachedAssets[asset.Id] = asset; - } - - return assets; - } - - public virtual async Task> QueryContentsAsync(string schemaIdOrName, string query) - { - var result = await contentQuery.QueryAsync(context, schemaIdOrName, Q.Empty.WithODataQuery(query)); - - foreach (var content in result) - { - cachedContents[content.Id] = content; - } - - return result; - } - - public virtual async Task> GetReferencedAssetsAsync(ICollection ids) - { - Guard.NotNull(ids, nameof(ids)); - - var notLoadedAssets = new HashSet(ids.Where(id => !cachedAssets.ContainsKey(id))); - - if (notLoadedAssets.Count > 0) - { - var assets = await assetQuery.QueryAsync(context, Q.Empty.WithIds(notLoadedAssets)); - - foreach (var asset in assets) - { - cachedAssets[asset.Id] = asset; - } - } - - return ids.Select(cachedAssets.GetOrDefault).Where(x => x != null).ToList(); - } - - public virtual async Task> GetReferencedContentsAsync(ICollection ids) - { - Guard.NotNull(ids, nameof(ids)); - - var notLoadedContents = ids.Where(id => !cachedContents.ContainsKey(id)).ToList(); - - if (notLoadedContents.Count > 0) - { - var result = await contentQuery.QueryAsync(context, notLoadedContents); - - foreach (var content in result) - { - cachedContents[content.Id] = content; - } - } - - return ids.Select(cachedContents.GetOrDefault).Where(x => x != null).ToList(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs deleted file mode 100644 index 5b06f2dc4..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.Contents.Repositories -{ - public interface IContentRepository - { - Task> QueryAsync(IAppEntity app, Status[] status, HashSet ids, bool includeDraft); - - Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids, bool includeDraft); - - Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, ClrQuery query, bool includeDraft); - - Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode); - - Task> QueryIdsAsync(Guid appId, HashSet ids); - - Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id, bool includeDraft); - - Task QueryScheduledWithoutDataAsync(Instant now, Func callback); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs deleted file mode 100644 index 5865a221d..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ /dev/null @@ -1,146 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.Serialization; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -#pragma warning disable IDE0060 // Remove unused parameter - -namespace Squidex.Domain.Apps.Entities.Contents.State -{ - public class ContentState : DomainObjectState, IContentEntity - { - [DataMember] - public NamedId AppId { get; set; } - - [DataMember] - public NamedId SchemaId { get; set; } - - [DataMember] - public NamedContentData Data { get; set; } - - [DataMember] - public NamedContentData DataDraft { get; set; } - - [DataMember] - public ScheduleJob ScheduleJob { get; set; } - - [DataMember] - public bool IsPending { get; set; } - - [DataMember] - public bool IsDeleted { get; set; } - - [DataMember] - public Status Status { get; set; } - - public void ApplyEvent(IEvent @event) - { - switch (@event) - { - case ContentCreated e: - { - SimpleMapper.Map(e, this); - - UpdateData(null, e.Data, false); - - break; - } - - case ContentChangesPublished _: - { - ScheduleJob = null; - - UpdateData(DataDraft, null, false); - - break; - } - - case ContentStatusChanged e: - { - ScheduleJob = null; - - SimpleMapper.Map(e, this); - - if (e.Status == Status.Published) - { - UpdateData(DataDraft, null, false); - } - - break; - } - - case ContentUpdated e: - { - UpdateData(e.Data, e.Data, false); - - break; - } - - case ContentUpdateProposed e: - { - UpdateData(null, e.Data, true); - - break; - } - - case ContentChangesDiscarded _: - { - UpdateData(null, Data, false); - - break; - } - - case ContentSchedulingCancelled _: - { - ScheduleJob = null; - - break; - } - - case ContentStatusScheduled e: - { - ScheduleJob = ScheduleJob.Build(e.Status, e.Actor, e.DueTime); - - break; - } - - case ContentDeleted _: - { - IsDeleted = true; - - break; - } - } - } - - public override ContentState Apply(Envelope @event) - { - return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); - } - - private void UpdateData(NamedContentData data, NamedContentData dataDraft, bool isPending) - { - if (data != null) - { - Data = data; - } - - if (dataDraft != null) - { - DataDraft = dataDraft; - } - - IsPending = isPending; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs deleted file mode 100644 index e4869b4e3..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs +++ /dev/null @@ -1,117 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - public sealed class GrainTextIndexer : ITextIndexer, IEventConsumer - { - private readonly IGrainFactory grainFactory; - - public string Name - { - get { return "TextIndexer"; } - } - - public string EventsFilter - { - get { return "^content-"; } - } - - public GrainTextIndexer(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public bool Handles(StoredEvent @event) - { - return true; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - - public async Task On(Envelope @event) - { - if (@event.Payload is ContentEvent contentEvent) - { - var index = grainFactory.GetGrain(contentEvent.SchemaId.Id); - - var id = contentEvent.ContentId; - - switch (@event.Payload) - { - case ContentDeleted _: - await index.DeleteAsync(id); - break; - case ContentCreated contentCreated: - await index.IndexAsync(Data(id, contentCreated.Data, true)); - break; - case ContentUpdateProposed contentUpdateProposed: - await index.IndexAsync(Data(id, contentUpdateProposed.Data, true)); - break; - case ContentUpdated contentUpdated: - await index.IndexAsync(Data(id, contentUpdated.Data, false)); - break; - case ContentChangesDiscarded _: - await index.CopyAsync(id, false); - break; - case ContentChangesPublished _: - case ContentStatusChanged contentStatusChanged when contentStatusChanged.Status == Status.Published: - await index.CopyAsync(id, true); - break; - } - } - } - - private static J Data(Guid contentId, NamedContentData data, bool onlySelf) - { - return new Update { Id = contentId, Data = data, OnlyDraft = onlySelf }; - } - - public async Task> SearchAsync(string queryText, IAppEntity app, Guid schemaId, Scope scope = Scope.Published) - { - if (string.IsNullOrWhiteSpace(queryText)) - { - return null; - } - - var index = grainFactory.GetGrain(schemaId); - - using (Profiler.TraceMethod()) - { - var context = CreateContext(app, scope); - - return await index.SearchAsync(queryText, context); - } - } - - private static SearchContext CreateContext(IAppEntity app, Scope scope) - { - var languages = new HashSet(app.LanguagesConfig.Select(x => x.Key)); - - return new SearchContext { Languages = languages, Scope = scope }; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs deleted file mode 100644 index 4e86a644e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps; - -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - public interface ITextIndexer - { - Task> SearchAsync(string queryText, IAppEntity app, Guid schemaId, Scope scope = Scope.Published); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexState.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexState.cs deleted file mode 100644 index 4a289c91b..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexState.cs +++ /dev/null @@ -1,144 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Lucene.Net.Documents; -using Lucene.Net.Index; -using Lucene.Net.Search; -using Lucene.Net.Util; - -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - internal sealed class IndexState - { - private const int NotFound = -1; - private const string MetaFor = "_fd"; - private readonly IndexSearcher indexSearcher; - private readonly IndexWriter indexWriter; - private readonly BinaryDocValues binaryValues; - private readonly Dictionary<(Guid, byte), BytesRef> changes = new Dictionary<(Guid, byte), BytesRef>(); - private bool isClosed; - - public IndexState(IndexWriter indexWriter, IndexReader indexReader = null, IndexSearcher indexSearcher = null) - { - this.indexSearcher = indexSearcher; - this.indexWriter = indexWriter; - - if (indexReader != null) - { - binaryValues = MultiDocValues.GetBinaryValues(indexReader, MetaFor); - } - } - - public void Index(Guid id, byte draft, Document document, byte forDraft, byte forPublished) - { - var value = GetValue(forDraft, forPublished); - - document.SetBinaryDocValue(MetaFor, value); - - changes[(id, draft)] = value; - } - - public void Index(Guid id, byte draft, Term term, byte forDraft, byte forPublished) - { - var value = GetValue(forDraft, forPublished); - - indexWriter.UpdateBinaryDocValue(term, MetaFor, value); - - changes[(id, draft)] = value; - } - - public bool HasBeenAdded(Guid id, byte draft, Term term, out int docId) - { - docId = 0; - - if (changes.ContainsKey((id, draft))) - { - return true; - } - - if (indexSearcher != null && !isClosed) - { - var docs = indexSearcher.Search(new TermQuery(term), 1); - - docId = docs?.ScoreDocs.FirstOrDefault()?.Doc ?? NotFound; - - return docId > NotFound; - } - - return false; - } - - public bool TryGet(Guid id, byte draft, int docId, out byte forDraft, out byte forPublished) - { - forDraft = 0; - forPublished = 0; - - if (changes.TryGetValue((id, draft), out var forValue)) - { - forDraft = forValue.Bytes[0]; - forPublished = forValue.Bytes[1]; - - return true; - } - - if (!isClosed && docId != NotFound) - { - forValue = new BytesRef(); - - binaryValues?.Get(docId, forValue); - - if (forValue.Bytes.Length == 2) - { - forDraft = forValue.Bytes[0]; - forPublished = forValue.Bytes[1]; - - changes[(id, draft)] = forValue; - - return true; - } - } - - return false; - } - - public bool TryGet(int docId, out byte forDraft, out byte forPublished) - { - forDraft = 0; - forPublished = 0; - - if (!isClosed && docId != NotFound) - { - var forValue = new BytesRef(); - - binaryValues?.Get(docId, forValue); - - if (forValue.Bytes.Length == 2) - { - forDraft = forValue.Bytes[0]; - forPublished = forValue.Bytes[1]; - - return true; - } - } - - return false; - } - - private static BytesRef GetValue(byte forDraft, byte forPublished) - { - return new BytesRef(new[] { forDraft, forPublished }); - } - - public void CloseReader() - { - isClosed = true; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs deleted file mode 100644 index 8bd41df9c..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Lucene.Net.Analysis; -using Lucene.Net.Analysis.Standard; -using Lucene.Net.Util; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - public sealed class MultiLanguageAnalyzer : AnalyzerWrapper - { - private readonly StandardAnalyzer fallbackAnalyzer; - private readonly Dictionary analyzers = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public MultiLanguageAnalyzer(LuceneVersion version) - : base(PER_FIELD_REUSE_STRATEGY) - { - fallbackAnalyzer = new StandardAnalyzer(version); - - foreach (var type in typeof(StandardAnalyzer).Assembly.GetTypes()) - { - if (typeof(Analyzer).IsAssignableFrom(type)) - { - var language = type.Namespace.Split('.').Last(); - - if (language.Length == 2) - { - try - { - var analyzer = Activator.CreateInstance(type, version); - - analyzers[language] = (Analyzer)analyzer; - } - catch (MissingMethodException) - { - continue; - } - } - } - } - } - - protected override Analyzer GetWrappedAnalyzer(string fieldName) - { - if (fieldName.Length > 0) - { - var analyzer = analyzers.GetOrDefault(fieldName.Substring(0, 2)) ?? fallbackAnalyzer; - - return analyzer; - } - else - { - return fallbackAnalyzer; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexContent.cs deleted file mode 100644 index 62fa1a033..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexContent.cs +++ /dev/null @@ -1,210 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Text; -using Lucene.Net.Documents; -using Lucene.Net.Index; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - internal sealed class TextIndexContent - { - private const string MetaId = "_id"; - private const string MetaKey = "_key"; - private readonly IndexWriter indexWriter; - private readonly IndexState indexState; - private readonly Guid id; - - public TextIndexContent(IndexWriter indexWriter, IndexState indexState, Guid id) - { - this.indexWriter = indexWriter; - this.indexState = indexState; - - this.id = id; - } - - public void Delete() - { - indexWriter.DeleteDocuments(new Term(MetaId, id.ToString())); - } - - public static bool TryGetId(int docId, Scope scope, IndexReader reader, IndexState indexState, out Guid result) - { - result = Guid.Empty; - - if (!indexState.TryGet(docId, out var draft, out var published)) - { - return false; - } - - if (scope == Scope.Draft && draft != 1) - { - return false; - } - - if (scope == Scope.Published && published != 1) - { - return false; - } - - var document = reader.Document(docId); - - var idString = document.Get(MetaId); - - if (!Guid.TryParse(idString, out result)) - { - return false; - } - - return true; - } - - public void Index(NamedContentData data, bool onlyDraft) - { - var converted = CreateDocument(data); - - Upsert(converted, 1, 1, 0); - - var isPublishDocumentAdded = IsAdded(0, out var docId); - var isPublishForPublished = IsForPublished(0, docId); - - if (!onlyDraft && isPublishDocumentAdded && isPublishForPublished) - { - Upsert(converted, 0, 0, 1); - } - else if (!onlyDraft || !isPublishDocumentAdded) - { - Upsert(converted, 0, 0, 0); - } - else - { - UpdateFor(0, 0, isPublishForPublished ? (byte)1 : (byte)0); - } - } - - public void Copy(bool fromDraft) - { - if (fromDraft) - { - UpdateFor(1, 1, 0); - UpdateFor(0, 0, 1); - } - else - { - UpdateFor(1, 0, 0); - UpdateFor(0, 1, 1); - } - } - - private static Document CreateDocument(NamedContentData data) - { - var languages = new Dictionary(); - - void AppendText(string language, string text) - { - if (!string.IsNullOrWhiteSpace(text)) - { - var sb = languages.GetOrAddNew(language); - - if (sb.Length > 0) - { - sb.Append(" "); - } - - sb.Append(text); - } - } - - foreach (var field in data) - { - foreach (var fieldValue in field.Value) - { - var appendText = new Action(text => AppendText(fieldValue.Key, text)); - - AppendJsonText(fieldValue.Value, appendText); - } - } - - var document = new Document(); - - foreach (var field in languages) - { - document.AddTextField(field.Key, field.Value.ToString(), Field.Store.NO); - } - - return document; - } - - private void UpdateFor(byte draft, byte forDraft, byte forPublished) - { - var term = new Term(MetaKey, BuildKey(draft)); - - indexState.Index(id, draft, term, forDraft, forPublished); - } - - private void Upsert(Document document, byte draft, byte forDraft, byte forPublished) - { - if (document != null) - { - document.RemoveField(MetaId); - document.RemoveField(MetaKey); - - var contentId = id.ToString(); - var contentKey = BuildKey(draft); - - document.AddStringField(MetaId, contentId, Field.Store.YES); - document.AddStringField(MetaKey, contentKey, Field.Store.YES); - - indexState.Index(id, draft, document, forDraft, forPublished); - - indexWriter.UpdateDocument(new Term(MetaKey, contentKey), document); - } - } - - private static void AppendJsonText(IJsonValue value, Action appendText) - { - if (value.Type == JsonValueType.String) - { - appendText(value.ToString()); - } - else if (value is JsonArray array) - { - foreach (var item in array) - { - AppendJsonText(item, appendText); - } - } - else if (value is JsonObject obj) - { - foreach (var item in obj.Values) - { - AppendJsonText(item, appendText); - } - } - } - - private bool IsAdded(byte draft, out int docId) - { - return indexState.HasBeenAdded(id, draft, new Term(MetaKey, BuildKey(draft)), out docId); - } - - private bool IsForPublished(byte draft, int docId) - { - return indexState.TryGet(id, draft, docId, out _, out var p) && p == 1; - } - - private string BuildKey(byte draft) - { - return $"{id}_{draft}"; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs deleted file mode 100644 index fb1133d9e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs +++ /dev/null @@ -1,259 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Lucene.Net.Analysis; -using Lucene.Net.Index; -using Lucene.Net.QueryParsers.Classic; -using Lucene.Net.Search; -using Lucene.Net.Store; -using Lucene.Net.Util; -using Squidex.Domain.Apps.Core; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - public sealed class TextIndexerGrain : GrainOfGuid, ITextIndexerGrain - { - private const LuceneVersion Version = LuceneVersion.LUCENE_48; - private const int MaxResults = 2000; - private const int MaxUpdates = 400; - private static readonly TimeSpan CommitDelay = TimeSpan.FromSeconds(10); - private static readonly MergeScheduler MergeScheduler = new ConcurrentMergeScheduler(); - private static readonly Analyzer Analyzer = new MultiLanguageAnalyzer(Version); - private static readonly string[] Invariant = { InvariantPartitioning.Key }; - private readonly SnapshotDeletionPolicy snapshotter = new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()); - private readonly IAssetStore assetStore; - private IDisposable timer; - private DirectoryInfo directory; - private IndexWriter indexWriter; - private IndexReader indexReader; - private IndexSearcher indexSearcher; - private IndexState indexState; - private QueryParser queryParser; - private HashSet currentLanguages; - private int updates; - - public TextIndexerGrain(IAssetStore assetStore) - { - Guard.NotNull(assetStore, nameof(assetStore)); - - this.assetStore = assetStore; - } - - public override async Task OnDeactivateAsync() - { - await DeactivateAsync(true); - } - - protected override async Task OnActivateAsync(Guid key) - { - directory = new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"Index_{key}")); - - await assetStore.DownloadAsync(directory); - - var config = new IndexWriterConfig(Version, Analyzer) - { - IndexDeletionPolicy = snapshotter, - MergePolicy = new TieredMergePolicy(), - MergeScheduler = MergeScheduler - }; - - indexWriter = new IndexWriter(FSDirectory.Open(directory), config); - - if (indexWriter.NumDocs > 0) - { - OpenReader(); - } - else - { - indexState = new IndexState(indexWriter); - } - } - - public Task IndexAsync(J update) - { - return IndexInternalAsync(update); - } - - private Task IndexInternalAsync(Update update) - { - var content = new TextIndexContent(indexWriter, indexState, update.Id); - - content.Index(update.Data, update.OnlyDraft); - - return TryFlushAsync(); - } - - public Task CopyAsync(Guid id, bool fromDraft) - { - var content = new TextIndexContent(indexWriter, indexState, id); - - content.Copy(fromDraft); - - return TryFlushAsync(); - } - - public Task DeleteAsync(Guid id) - { - var content = new TextIndexContent(indexWriter, indexState, id); - - content.Delete(); - - return TryFlushAsync(); - } - - public Task> SearchAsync(string queryText, SearchContext context) - { - var result = new List(); - - if (!string.IsNullOrWhiteSpace(queryText)) - { - var query = BuildQuery(queryText, context); - - if (indexReader == null && indexWriter.NumDocs > 0) - { - OpenReader(); - } - - if (indexReader != null) - { - var found = new HashSet(); - - var hits = indexSearcher.Search(query, MaxResults).ScoreDocs; - - foreach (var hit in hits) - { - if (TextIndexContent.TryGetId(hit.Doc, context.Scope, indexReader, indexState, out var id)) - { - if (found.Add(id)) - { - result.Add(id); - } - } - } - } - } - - return Task.FromResult(result.ToList()); - } - - private Query BuildQuery(string query, SearchContext context) - { - if (queryParser == null || !currentLanguages.SetEquals(context.Languages)) - { - var fields = context.Languages.Union(Invariant).ToArray(); - - queryParser = new MultiFieldQueryParser(Version, fields, Analyzer); - - currentLanguages = context.Languages; - } - - try - { - return queryParser.Parse(query); - } - catch (ParseException ex) - { - throw new ValidationException(ex.Message); - } - } - - private async Task TryFlushAsync() - { - timer?.Dispose(); - - updates++; - - if (updates >= MaxUpdates) - { - await FlushAsync(); - - return true; - } - else - { - CleanReader(); - - try - { - timer = RegisterTimer(_ => FlushAsync(), null, CommitDelay, CommitDelay); - } - catch (InvalidOperationException) - { - return false; - } - } - - return false; - } - - public async Task FlushAsync() - { - if (updates > 0 && indexWriter != null) - { - indexWriter.Commit(); - indexWriter.Flush(true, true); - - CleanReader(); - - var commit = snapshotter.Snapshot(); - try - { - await assetStore.UploadDirectoryAsync(directory, commit); - } - finally - { - snapshotter.Release(commit); - } - - updates = 0; - } - } - - public async Task DeactivateAsync(bool deleteFolder = false) - { - await FlushAsync(); - - CleanWriter(); - CleanReader(); - - if (deleteFolder && directory.Exists) - { - directory.Delete(true); - } - } - - private void OpenReader() - { - indexReader = indexWriter.GetReader(true); - indexSearcher = new IndexSearcher(indexReader); - indexState = new IndexState(indexWriter, indexReader, indexSearcher); - } - - private void CleanReader() - { - indexReader?.Dispose(); - indexReader = null; - indexSearcher = null; - indexState?.CloseReader(); - } - - private void CleanWriter() - { - indexWriter?.Dispose(); - indexWriter = null; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Context.cs b/src/Squidex.Domain.Apps.Entities/Context.cs deleted file mode 100644 index 721b12a7a..000000000 --- a/src/Squidex.Domain.Apps.Entities/Context.cs +++ /dev/null @@ -1,71 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Security.Claims; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Shared; -using Squidex.Shared.Identity; -using ClaimsPermissions = Squidex.Infrastructure.Security.PermissionSet; - -namespace Squidex.Domain.Apps.Entities -{ - public sealed class Context - { - public IDictionary Headers { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public IAppEntity App { get; set; } - - public ClaimsPrincipal User { get; } - - public ClaimsPermissions Permissions { get; private set; } = ClaimsPermissions.Empty; - - public bool IsFrontendClient { get; private set; } - - public Context(ClaimsPrincipal user) - { - Guard.NotNull(user, nameof(user)); - - User = user; - - UpdatePermissions(); - } - - public Context(ClaimsPrincipal user, IAppEntity app) - : this(user) - { - App = app; - } - - public static Context Anonymous() - { - return new Context(new ClaimsPrincipal()); - } - - public void UpdatePermissions() - { - Permissions = User.Permissions(); - - IsFrontendClient = User.IsInClient(DefaultClients.Frontend); - } - - public Context Clone() - { - var clone = new Context(User, App); - - foreach (var kvp in Headers) - { - clone.Headers[kvp.Key] = kvp.Value; - } - - return clone; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs deleted file mode 100644 index 8c8d5dfbf..000000000 --- a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs +++ /dev/null @@ -1,82 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities -{ - public static class EntityMapper - { - public static T Update(this T entity, Envelope envelope, Action updater = null) where T : IEntity - { - var @event = (SquidexEvent)envelope.Payload; - - var headers = envelope.Headers; - - SetId(entity, headers); - SetCreated(entity, headers); - SetCreatedBy(entity, @event); - SetLastModified(entity, headers); - SetLastModifiedBy(entity, @event); - SetVersion(entity, headers); - - updater?.Invoke(@event, entity); - - return entity; - } - - private static void SetId(IEntity entity, EnvelopeHeaders headers) - { - if (entity is IUpdateableEntity updateable && updateable.Id == Guid.Empty) - { - updateable.Id = headers.AggregateId(); - } - } - - private static void SetVersion(IEntity entity, EnvelopeHeaders headers) - { - if (entity is IUpdateableEntityWithVersion updateable) - { - updateable.Version = headers.EventStreamNumber(); - } - } - - private static void SetCreated(IEntity entity, EnvelopeHeaders headers) - { - if (entity is IUpdateableEntity updateable && updateable.Created == default) - { - updateable.Created = headers.Timestamp(); - } - } - - private static void SetCreatedBy(IEntity entity, SquidexEvent @event) - { - if (entity is IUpdateableEntityWithCreatedBy withCreatedBy && withCreatedBy.CreatedBy == null) - { - withCreatedBy.CreatedBy = @event.Actor; - } - } - - private static void SetLastModified(IEntity entity, EnvelopeHeaders headers) - { - if (entity is IUpdateableEntity updateable) - { - updateable.LastModified = headers.Timestamp(); - } - } - - private static void SetLastModifiedBy(IEntity entity, SquidexEvent @event) - { - if (entity is IUpdateableEntityWithLastModifiedBy withModifiedBy) - { - withModifiedBy.LastModifiedBy = @event.Actor; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs b/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs deleted file mode 100644 index 60dec6de4..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NodaTime; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.History -{ - public sealed class HistoryEvent - { - public Guid Id { get; set; } = Guid.NewGuid(); - - public Guid AppId { get; set; } - - public Instant Created { get; set; } - - public RefToken Actor { get; set; } - - public long Version { get; set; } - - public string Channel { get; set; } - - public string Message { get; set; } - - public Dictionary Parameters { get; set; } = new Dictionary(); - - public HistoryEvent() - { - } - - public HistoryEvent(string channel, string message) - { - Guard.NotNullOrEmpty(channel, nameof(channel)); - Guard.NotNullOrEmpty(message, nameof(message)); - - Channel = channel; - - Message = message; - } - - public HistoryEvent Param(string key, T value) - { - if (!Equals(value, default(T))) - { - Parameters[key] = value.ToString(); - } - - return this; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs b/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs deleted file mode 100644 index 9e873e694..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.History -{ - public abstract class HistoryEventsCreatorBase : IHistoryEventsCreator - { - private readonly Dictionary texts = new Dictionary(); - private readonly TypeNameRegistry typeNameRegistry; - - public IReadOnlyDictionary Texts - { - get { return texts; } - } - - protected HistoryEventsCreatorBase(TypeNameRegistry typeNameRegistry) - { - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); - - this.typeNameRegistry = typeNameRegistry; - } - - protected void AddEventMessage(string message) where TEvent : IEvent - { - Guard.NotNullOrEmpty(message, nameof(message)); - - texts[typeNameRegistry.GetName()] = message; - } - - protected bool HasEventText(IEvent @event) - { - var message = typeNameRegistry.GetName(@event.GetType()); - - return texts.ContainsKey(message); - } - - protected HistoryEvent ForEvent(IEvent @event, string channel) - { - var message = typeNameRegistry.GetName(@event.GetType()); - - return new HistoryEvent(channel, message); - } - - public Task CreateEventAsync(Envelope @event) - { - if (HasEventText(@event.Payload)) - { - return CreateEventCoreAsync(@event); - } - - return Task.FromResult(null); - } - - protected abstract Task CreateEventCoreAsync(Envelope @event); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs b/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs deleted file mode 100644 index e0f8e00c2..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs +++ /dev/null @@ -1,90 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.History.Repositories; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities.History -{ - public sealed class HistoryService : IHistoryService, IEventConsumer - { - private readonly Dictionary texts = new Dictionary(); - private readonly List creators; - private readonly IHistoryEventRepository repository; - - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return ".*"; } - } - - public HistoryService(IHistoryEventRepository repository, IEnumerable creators) - { - Guard.NotNull(repository, nameof(repository)); - Guard.NotNull(creators, nameof(creators)); - - this.creators = creators.ToList(); - - foreach (var creator in this.creators) - { - foreach (var text in creator.Texts) - { - texts[text.Key] = text.Value; - } - } - - this.repository = repository; - } - - public bool Handles(StoredEvent @event) - { - return true; - } - - public Task ClearAsync() - { - return repository.ClearAsync(); - } - - public async Task On(Envelope @event) - { - foreach (var creator in creators) - { - var historyEvent = await creator.CreateEventAsync(@event); - - if (historyEvent != null) - { - var appEvent = (AppEvent)@event.Payload; - - historyEvent.Actor = appEvent.Actor; - historyEvent.AppId = appEvent.AppId.Id; - historyEvent.Created = @event.Headers.Timestamp(); - historyEvent.Version = @event.Headers.EventStreamNumber(); - - await repository.InsertAsync(historyEvent); - } - } - } - - public async Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count) - { - var items = await repository.QueryByChannelAsync(appId, channelPrefix, count); - - return items.Select(x => new ParsedHistoryEvent(x, texts)).ToList(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs deleted file mode 100644 index 5b15f92d9..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities.History -{ - public interface IHistoryEventsCreator - { - IReadOnlyDictionary Texts { get; } - - Task CreateEventAsync(Envelope @event); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailEventConsumer.cs b/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailEventConsumer.cs deleted file mode 100644 index 3a26e2814..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailEventConsumer.cs +++ /dev/null @@ -1,121 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using NodaTime; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.History.Notifications -{ - public sealed class NotificationEmailEventConsumer : IEventConsumer - { - private static readonly Duration MaxAge = Duration.FromDays(2); - private readonly INotificationEmailSender emailSender; - private readonly IUserResolver userResolver; - private readonly ISemanticLog log; - - public string Name - { - get { return "NotificationEmailSender"; } - } - - public string EventsFilter - { - get { return "^app-"; } - } - - public NotificationEmailEventConsumer(INotificationEmailSender emailSender, IUserResolver userResolver, ISemanticLog log) - { - Guard.NotNull(emailSender, nameof(emailSender)); - Guard.NotNull(userResolver, nameof(userResolver)); - Guard.NotNull(log, nameof(log)); - - this.emailSender = emailSender; - this.userResolver = userResolver; - - this.log = log; - } - - public bool Handles(StoredEvent @event) - { - return true; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - - public async Task On(Envelope @event) - { - if (!emailSender.IsActive) - { - return; - } - - if (@event.Headers.EventStreamNumber() <= 1) - { - return; - } - - var now = SystemClock.Instance.GetCurrentInstant(); - - var timestamp = @event.Headers.Timestamp(); - - if (now - timestamp > MaxAge) - { - return; - } - - if (@event.Payload is AppContributorAssigned appContributorAssigned) - { - if (!appContributorAssigned.Actor.IsSubject || !appContributorAssigned.IsAdded) - { - return; - } - - var assignerId = appContributorAssigned.Actor.Identifier; - var assigneeId = appContributorAssigned.ContributorId; - - var assigner = await userResolver.FindByIdOrEmailAsync(assignerId); - - if (assigner == null) - { - LogWarning($"Assigner {assignerId} not found"); - return; - } - - var assignee = await userResolver.FindByIdOrEmailAsync(appContributorAssigned.ContributorId); - - if (assignee == null) - { - LogWarning($"Assignee {assigneeId} not found"); - return; - } - - var appName = appContributorAssigned.AppId.Name; - - var isCreated = appContributorAssigned.IsCreated; - - await emailSender.SendContributorEmailAsync(assigner, assignee, appName, isCreated); - } - } - - private void LogWarning(string reason) - { - log.LogWarning(w => w - .WriteProperty("action", "InviteUser") - .WriteProperty("status", "Failed") - .WriteProperty("reason", reason)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailSender.cs b/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailSender.cs deleted file mode 100644 index 8ee529484..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailSender.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Email; -using Squidex.Infrastructure.Log; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.History.Notifications -{ - public sealed class NotificationEmailSender : INotificationEmailSender - { - private readonly IEmailSender emailSender; - private readonly IEmailUrlGenerator emailUrlGenerator; - private readonly ISemanticLog log; - private readonly NotificationEmailTextOptions texts; - - public bool IsActive - { - get { return true; } - } - - public NotificationEmailSender( - IOptions texts, - IEmailSender emailSender, - IEmailUrlGenerator emailUrlGenerator, - ISemanticLog log) - { - Guard.NotNull(texts, nameof(texts)); - Guard.NotNull(emailSender, nameof(emailSender)); - Guard.NotNull(emailUrlGenerator, nameof(emailUrlGenerator)); - Guard.NotNull(log, nameof(log)); - - this.texts = texts.Value; - this.emailSender = emailSender; - this.emailUrlGenerator = emailUrlGenerator; - this.log = log; - } - - public Task SendContributorEmailAsync(IUser assigner, IUser assignee, string appName, bool isCreated) - { - Guard.NotNull(assigner, nameof(assigner)); - Guard.NotNull(assignee, nameof(assignee)); - Guard.NotNull(appName, nameof(appName)); - - if (assignee.HasConsent()) - { - return SendEmailAsync(texts.ExistingUserSubject, texts.ExistingUserBody, assigner, assignee, appName); - } - else - { - return SendEmailAsync(texts.NewUserSubject, texts.NewUserBody, assigner, assignee, appName); - } - } - - private async Task SendEmailAsync(string emailSubj, string emailBody, IUser assigner, IUser assignee, string appName) - { - if (string.IsNullOrWhiteSpace(emailBody)) - { - LogWarning("No email subject configured for new users"); - return; - } - - if (string.IsNullOrWhiteSpace(emailSubj)) - { - LogWarning("No email body configured for new users"); - return; - } - - var appUrl = emailUrlGenerator.GenerateUIUrl(); - - emailSubj = Format(emailSubj, assigner, assignee, appUrl, appName); - emailBody = Format(emailBody, assigner, assignee, appUrl, appName); - - await emailSender.SendAsync(assignee.Email, emailSubj, emailBody); - } - - private void LogWarning(string reason) - { - log.LogWarning(w => w - .WriteProperty("action", "InviteUser") - .WriteProperty("status", "Failed") - .WriteProperty("reason", reason)); - } - - private static string Format(string text, IUser assigner, IUser assignee, string uiUrl, string appName) - { - text = text.Replace("$APP_NAME", appName); - - if (assigner != null) - { - text = text.Replace("$ASSIGNER_EMAIL", assigner.Email); - text = text.Replace("$ASSIGNER_NAME", assigner.DisplayName()); - } - - if (assignee != null) - { - text = text.Replace("$ASSIGNEE_EMAIL", assignee.Email); - text = text.Replace("$ASSIGNEE_NAME", assignee.DisplayName()); - } - - text = text.Replace("$UI_URL", uiUrl); - - return text; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs b/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs deleted file mode 100644 index 7e5c018b3..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NodaTime; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.History -{ - public sealed class ParsedHistoryEvent - { - private readonly HistoryEvent item; - private readonly Lazy message; - - public Guid Id - { - get { return item.Id; } - } - - public Instant Created - { - get { return item.Created; } - } - - public RefToken Actor - { - get { return item.Actor; } - } - - public long Version - { - get { return item.Version; } - } - - public string Channel - { - get { return item.Channel; } - } - - public string Message - { - get { return message.Value; } - } - - public ParsedHistoryEvent(HistoryEvent item, IReadOnlyDictionary texts) - { - this.item = item; - - message = new Lazy(() => - { - if (texts.TryGetValue(item.Message, out var result)) - { - foreach (var kvp in item.Parameters) - { - result = result.Replace("[" + kvp.Key + "]", kvp.Value); - } - - return result; - } - - return null; - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/IAppProvider.cs b/src/Squidex.Domain.Apps.Entities/IAppProvider.cs deleted file mode 100644 index 8fa6200dc..000000000 --- a/src/Squidex.Domain.Apps.Entities/IAppProvider.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure.Security; - -namespace Squidex.Domain.Apps.Entities -{ - public interface IAppProvider - { - Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id); - - Task GetAppAsync(Guid appId); - - Task GetAppAsync(string appName); - - Task> GetUserAppsAsync(string userId, PermissionSet permissions); - - Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false); - - Task GetSchemaAsync(Guid appId, string name); - - Task> GetSchemasAsync(Guid appId); - - Task> GetRulesAsync(Guid appId); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs b/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs deleted file mode 100644 index 1a6b5af96..000000000 --- a/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; - -namespace Squidex.Domain.Apps.Entities -{ - public interface IEntityWithCacheDependencies - { - HashSet CacheDependencies { get; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Q.cs b/src/Squidex.Domain.Apps.Entities/Q.cs deleted file mode 100644 index 9fce3c394..000000000 --- a/src/Squidex.Domain.Apps.Entities/Q.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities -{ - public sealed class Q : Cloneable - { - public static readonly Q Empty = new Q(); - - public IReadOnlyList Ids { get; private set; } - - public string ODataQuery { get; private set; } - - public string JsonQuery { get; private set; } - - public Q WithODataQuery(string odataQuery) - { - return Clone(c => c.ODataQuery = odataQuery); - } - - public Q WithJsonQuery(string jsonQuery) - { - return Clone(c => c.JsonQuery = jsonQuery); - } - - public Q WithIds(params Guid[] ids) - { - return Clone(c => c.Ids = ids.ToList()); - } - - public Q WithIds(IEnumerable ids) - { - return Clone(c => c.Ids = ids.ToList()); - } - - public Q WithIds(string ids) - { - if (!string.IsNullOrEmpty(ids)) - { - return Clone(c => - { - var idsList = new List(); - - foreach (var id in ids.Split(',')) - { - if (Guid.TryParse(id, out var guid)) - { - idsList.Add(guid); - } - } - - c.Ids = idsList; - }); - } - - return this; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs b/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs deleted file mode 100644 index f021bdc16..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Entities.Rules.Indexes; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public sealed class BackupRules : BackupHandler - { - private readonly HashSet ruleIds = new HashSet(); - private readonly IRulesIndex indexForRules; - - public override string Name { get; } = "Rules"; - - public BackupRules(IRulesIndex indexForRules) - { - Guard.NotNull(indexForRules, nameof(indexForRules)); - - this.indexForRules = indexForRules; - } - - public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) - { - switch (@event.Payload) - { - case RuleCreated ruleCreated: - ruleIds.Add(ruleCreated.RuleId); - break; - case RuleDeleted ruleDeleted: - ruleIds.Remove(ruleDeleted.RuleId); - break; - } - - return TaskHelper.True; - } - - public override Task RestoreAsync(Guid appId, BackupReader reader) - { - return indexForRules.RebuildAsync(appId, ruleIds); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs deleted file mode 100644 index 3ea443623..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs +++ /dev/null @@ -1,106 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Rules.Guards -{ - public static class GuardRule - { - public static Task CanCreate(CreateRule command, IAppProvider appProvider) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot create rule.", async e => - { - if (command.Trigger == null) - { - e(Not.Defined("Trigger"), nameof(command.Trigger)); - } - else - { - var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Id, command.Trigger, appProvider); - - errors.Foreach(x => x.AddTo(e)); - } - - if (command.Action == null) - { - e(Not.Defined("Action"), nameof(command.Action)); - } - else - { - var errors = command.Action.Validate(); - - errors.Foreach(x => x.AddTo(e)); - } - }); - } - - public static Task CanUpdate(UpdateRule command, Guid appId, IAppProvider appProvider, Rule rule) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot update rule.", async e => - { - if (command.Trigger == null && command.Action == null && command.Name == null) - { - e(Not.Defined("Either trigger, action or name"), nameof(command.Trigger), nameof(command.Action)); - } - - if (command.Trigger != null) - { - var errors = await RuleTriggerValidator.ValidateAsync(appId, command.Trigger, appProvider); - - errors.Foreach(x => x.AddTo(e)); - } - - if (command.Action != null) - { - var errors = command.Action.Validate(); - - errors.Foreach(x => x.AddTo(e)); - } - - if (command.Name != null && string.Equals(rule.Name, command.Name)) - { - e(Not.New("Rule", "name"), nameof(command.Name)); - } - }); - } - - public static void CanEnable(EnableRule command, Rule rule) - { - Guard.NotNull(command, nameof(command)); - - if (rule.IsEnabled) - { - throw new DomainException("Rule is already enabled."); - } - } - - public static void CanDisable(DisableRule command, Rule rule) - { - Guard.NotNull(command, nameof(command)); - - if (!rule.IsEnabled) - { - throw new DomainException("Rule is already disabled."); - } - } - - public static void CanDelete(DeleteRule command) - { - Guard.NotNull(command, nameof(command)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs deleted file mode 100644 index 4b4f1829d..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs +++ /dev/null @@ -1,104 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Rules.Guards -{ - public sealed class RuleTriggerValidator : IRuleTriggerVisitor>> - { - public Func> SchemaProvider { get; } - - public RuleTriggerValidator(Func> schemaProvider) - { - SchemaProvider = schemaProvider; - } - - public static Task> ValidateAsync(Guid appId, RuleTrigger action, IAppProvider appProvider) - { - Guard.NotNull(action, nameof(action)); - Guard.NotNull(appProvider, nameof(appProvider)); - - var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appId, x)); - - return action.Accept(visitor); - } - - public Task> Visit(AssetChangedTriggerV2 trigger) - { - return Task.FromResult(Enumerable.Empty()); - } - - public Task> Visit(ManualTrigger trigger) - { - return Task.FromResult(Enumerable.Empty()); - } - - public Task> Visit(SchemaChangedTrigger trigger) - { - return Task.FromResult(Enumerable.Empty()); - } - - public Task> Visit(UsageTrigger trigger) - { - var errors = new List(); - - if (trigger.NumDays.HasValue && (trigger.NumDays < 1 || trigger.NumDays > 30)) - { - errors.Add(new ValidationError(Not.Between("Num days", 1, 30), nameof(trigger.NumDays))); - } - - return Task.FromResult>(errors); - } - - public async Task> Visit(ContentChangedTriggerV2 trigger) - { - var errors = new List(); - - if (trigger.Schemas != null) - { - var tasks = new List>(); - - foreach (var schema in trigger.Schemas) - { - if (schema.SchemaId == Guid.Empty) - { - errors.Add(new ValidationError(Not.Defined("Schema id"), nameof(trigger.Schemas))); - } - else - { - tasks.Add(CheckSchemaAsync(schema)); - } - } - - var checkErrors = await Task.WhenAll(tasks); - - errors.AddRange(checkErrors.Where(x => x != null)); - } - - return errors; - } - - private async Task CheckSchemaAsync(ContentChangedTriggerSchemaV2 schema) - { - if (await SchemaProvider(schema.SchemaId) == null) - { - return new ValidationError($"Schema {schema.SchemaId} does not exist.", nameof(ContentChangedTriggerV2.Schemas)); - } - - return null; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs b/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs deleted file mode 100644 index aaa2faeb1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public interface IRuleEventEntity : IEntity - { - RuleJob Job { get; } - - Instant? NextAttempt { get; } - - RuleJobResult JobResult { get; } - - RuleResult Result { get; } - - int NumCalls { get; } - - string LastDump { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs deleted file mode 100644 index f57d9154f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; - -namespace Squidex.Domain.Apps.Entities.Rules.Indexes -{ - public sealed class RulesIndex : ICommandMiddleware, IRulesIndex - { - private readonly IGrainFactory grainFactory; - - public RulesIndex(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public Task RebuildAsync(Guid appId, HashSet rues) - { - return Index(appId).RebuildAsync(rues); - } - - public async Task> GetRulesAsync(Guid appId) - { - using (Profiler.TraceMethod()) - { - var ids = await GetRuleIdsAsync(appId); - - var rules = - await Task.WhenAll( - ids.Select(GetRuleAsync)); - - return rules.Where(x => x != null).ToList(); - } - } - - private async Task GetRuleAsync(Guid id) - { - using (Profiler.TraceMethod()) - { - var ruleEntity = await grainFactory.GetGrain(id).GetStateAsync(); - - if (IsFound(ruleEntity.Value)) - { - return ruleEntity.Value; - } - - return null; - } - } - - private async Task> GetRuleIdsAsync(Guid appId) - { - using (Profiler.TraceMethod()) - { - return await Index(appId).GetIdsAsync(); - } - } - - public async Task HandleAsync(CommandContext context, Func next) - { - await next(); - - if (context.IsCompleted) - { - switch (context.Command) - { - case CreateRule createRule: - await CreateRuleAsync(createRule); - break; - case DeleteRule deleteRule: - await DeleteRuleAsync(deleteRule); - break; - } - } - } - - private async Task CreateRuleAsync(CreateRule command) - { - await Index(command.AppId.Id).AddAsync(command.RuleId); - } - - private async Task DeleteRuleAsync(DeleteRule command) - { - var id = command.RuleId; - - var rule = await grainFactory.GetGrain(id).GetStateAsync(); - - if (IsFound(rule.Value)) - { - await Index(rule.Value.AppId.Id).RemoveAsync(id); - } - } - - private IRulesByAppIndexGrain Index(Guid appId) - { - return grainFactory.GetGrain(appId); - } - - private static bool IsFound(IRuleEntity rule) - { - return rule.Version > EtagVersion.Empty && !rule.IsDeleted; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs deleted file mode 100644 index 05b3bc5c1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public sealed class ManualTriggerHandler : RuleTriggerHandler - { - protected override Task CreateEnrichedEventAsync(Envelope @event) - { - var result = new EnrichedManualEvent - { - Name = "Manual" - }; - - return Task.FromResult(result); - } - - protected override bool Trigger(EnrichedManualEvent @event, ManualTrigger trigger) - { - return true; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs b/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs deleted file mode 100644 index 01a0024e2..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Rules.Queries -{ - public sealed class RuleEnricher : IRuleEnricher - { - private readonly IRuleEventRepository ruleEventRepository; - - public RuleEnricher(IRuleEventRepository ruleEventRepository) - { - Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); - - this.ruleEventRepository = ruleEventRepository; - } - - public async Task EnrichAsync(IRuleEntity rule, Context context) - { - Guard.NotNull(rule, nameof(rule)); - - var enriched = await EnrichAsync(Enumerable.Repeat(rule, 1), context); - - return enriched[0]; - } - - public async Task> EnrichAsync(IEnumerable rules, Context context) - { - Guard.NotNull(rules, nameof(rules)); - Guard.NotNull(context, nameof(context)); - - using (Profiler.TraceMethod()) - { - var results = new List(); - - foreach (var rule in rules) - { - var result = SimpleMapper.Map(rule, new RuleEntity()); - - results.Add(result); - } - - foreach (var group in results.GroupBy(x => x.AppId.Id)) - { - var statistics = await ruleEventRepository.QueryStatisticsByAppAsync(group.Key); - - foreach (var rule in group) - { - var statistic = statistics.FirstOrDefault(x => x.RuleId == rule.Id); - - if (statistic != null) - { - rule.LastExecuted = statistic.LastExecuted; - rule.NumFailed = statistic.NumFailed; - rule.NumSucceeded = statistic.NumSucceeded; - - rule.CacheDependencies = new HashSet - { - statistic.LastExecuted - }; - } - } - } - - return results; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs b/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs deleted file mode 100644 index e6979e9bc..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; - -namespace Squidex.Domain.Apps.Entities.Rules.Repositories -{ - public interface IRuleEventRepository - { - Task EnqueueAsync(RuleJob job, Instant nextAttempt); - - Task EnqueueAsync(Guid id, Instant nextAttempt); - - Task CancelAsync(Guid id); - - Task MarkSentAsync(RuleJob job, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant finished, Instant? nextCall); - - Task QueryPendingAsync(Instant now, Func callback, CancellationToken ct = default); - - Task CountByAppAsync(Guid appId); - - Task> QueryStatisticsByAppAsync(Guid appId); - - Task> QueryByAppAsync(Guid appId, Guid? ruleId = null, int skip = 0, int take = 20); - - Task FindAsync(Guid id); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs deleted file mode 100644 index be304a805..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs +++ /dev/null @@ -1,163 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; -using NodaTime; -using Orleans; -using Orleans.Runtime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public class RuleDequeuerGrain : Grain, IRuleDequeuerGrain, IRemindable - { - private readonly ITargetBlock requestBlock; - private readonly IRuleEventRepository ruleEventRepository; - private readonly RuleService ruleService; - private readonly ConcurrentDictionary executing = new ConcurrentDictionary(); - private readonly IClock clock; - private readonly ISemanticLog log; - - public RuleDequeuerGrain(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log, IClock clock) - { - Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); - Guard.NotNull(ruleService, nameof(ruleService)); - Guard.NotNull(clock, nameof(clock)); - Guard.NotNull(log, nameof(log)); - - this.ruleEventRepository = ruleEventRepository; - this.ruleService = ruleService; - - this.clock = clock; - - this.log = log; - - requestBlock = - new PartitionedActionBlock(HandleAsync, x => x.Job.ExecutionPartition, - new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); - } - - public override Task OnActivateAsync() - { - DelayDeactivation(TimeSpan.FromDays(1)); - - RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); - RegisterTimer(x => QueryAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); - - return Task.FromResult(true); - } - - public override Task OnDeactivateAsync() - { - requestBlock.Complete(); - - return requestBlock.Completion; - } - - public Task ActivateAsync() - { - return TaskHelper.Done; - } - - public async Task QueryAsync() - { - try - { - var now = clock.GetCurrentInstant(); - - await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync); - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "QueueWebhookEvents") - .WriteProperty("status", "Failed")); - } - } - - public async Task HandleAsync(IRuleEventEntity @event) - { - if (!executing.TryAdd(@event.Id, false)) - { - return; - } - - try - { - var job = @event.Job; - - var (response, elapsed) = await ruleService.InvokeAsync(job.ActionName, job.ActionData); - - var jobInvoke = ComputeJobInvoke(response.Status, @event, job); - var jobResult = ComputeJobResult(response.Status, jobInvoke); - - var now = clock.GetCurrentInstant(); - - await ruleEventRepository.MarkSentAsync(@event.Job, response.Dump, response.Status, jobResult, elapsed, now, jobInvoke); - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "SendWebhookEvent") - .WriteProperty("status", "Failed")); - } - finally - { - executing.TryRemove(@event.Id, out _); - } - } - - private static RuleJobResult ComputeJobResult(RuleResult result, Instant? nextCall) - { - if (result != RuleResult.Success && !nextCall.HasValue) - { - return RuleJobResult.Failed; - } - else if (result != RuleResult.Success && nextCall.HasValue) - { - return RuleJobResult.Retry; - } - else - { - return RuleJobResult.Success; - } - } - - private static Instant? ComputeJobInvoke(RuleResult result, IRuleEventEntity @event, RuleJob job) - { - if (result != RuleResult.Success) - { - switch (@event.NumCalls) - { - case 0: - return job.Created.Plus(Duration.FromMinutes(5)); - case 1: - return job.Created.Plus(Duration.FromHours(1)); - case 2: - return job.Created.Plus(Duration.FromHours(6)); - case 3: - return job.Created.Plus(Duration.FromHours(12)); - } - } - - return null; - } - - public Task ReceiveReminder(string reminderName, TickStatus status) - { - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs deleted file mode 100644 index 82564f224..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public sealed class RuleEnqueuer : IEventConsumer, IRuleEnqueuer - { - private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(10); - private readonly IRuleEventRepository ruleEventRepository; - private readonly IAppProvider appProvider; - private readonly IMemoryCache cache; - private readonly RuleService ruleService; - - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return ".*"; } - } - - public RuleEnqueuer(IAppProvider appProvider, IMemoryCache cache, IRuleEventRepository ruleEventRepository, - RuleService ruleService) - { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(cache, nameof(cache)); - Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); - Guard.NotNull(ruleService, nameof(ruleService)); - - this.appProvider = appProvider; - - this.cache = cache; - - this.ruleEventRepository = ruleEventRepository; - this.ruleService = ruleService; - } - - public bool Handles(StoredEvent @event) - { - return true; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - - public async Task Enqueue(Rule rule, Guid ruleId, Envelope @event) - { - Guard.NotNull(rule, nameof(rule)); - Guard.NotNull(@event, nameof(@event)); - - var job = await ruleService.CreateJobAsync(rule, ruleId, @event); - - if (job != null) - { - await ruleEventRepository.EnqueueAsync(job, job.Created); - } - } - - public async Task On(Envelope @event) - { - if (@event.Payload is AppEvent appEvent) - { - var rules = await GetRulesAsync(appEvent.AppId.Id); - - foreach (var ruleEntity in rules) - { - await Enqueue(ruleEntity.RuleDef, ruleEntity.Id, @event); - } - } - } - - private Task> GetRulesAsync(Guid appId) - { - return cache.GetOrCreateAsync(appId, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheDuration; - - return appProvider.GetRulesAsync(appId); - }); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs deleted file mode 100644 index 373f9db95..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NodaTime; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public sealed class RuleEntity : IEnrichedRuleEntity - { - public Guid Id { get; set; } - - public NamedId AppId { get; set; } - - public NamedId SchemaId { get; set; } - - public long Version { get; set; } - - public Instant Created { get; set; } - - public Instant LastModified { get; set; } - - public RefToken CreatedBy { get; set; } - - public RefToken LastModifiedBy { get; set; } - - public Rule RuleDef { get; set; } - - public bool IsDeleted { get; set; } - - public int NumSucceeded { get; set; } - - public int NumFailed { get; set; } - - public Instant? LastExecuted { get; set; } - - public HashSet CacheDependencies { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs deleted file mode 100644 index 72993be18..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs +++ /dev/null @@ -1,154 +0,0 @@ -// ========================================================================== -// 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.Entities.Rules.Commands; -using Squidex.Domain.Apps.Entities.Rules.Guards; -using Squidex.Domain.Apps.Entities.Rules.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public sealed class RuleGrain : DomainObjectGrain, IRuleGrain - { - private readonly IAppProvider appProvider; - private readonly IRuleEnqueuer ruleEnqueuer; - - public RuleGrain(IStore store, ISemanticLog log, IAppProvider appProvider, IRuleEnqueuer ruleEnqueuer) - : base(store, log) - { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(ruleEnqueuer, nameof(ruleEnqueuer)); - - this.appProvider = appProvider; - - this.ruleEnqueuer = ruleEnqueuer; - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - VerifyNotDeleted(); - - switch (command) - { - case CreateRule createRule: - return CreateReturnAsync(createRule, async c => - { - await GuardRule.CanCreate(c, appProvider); - - Create(c); - - return Snapshot; - }); - case UpdateRule updateRule: - return UpdateReturnAsync(updateRule, async c => - { - await GuardRule.CanUpdate(c, Snapshot.AppId.Id, appProvider, Snapshot.RuleDef); - - Update(c); - - return Snapshot; - }); - case EnableRule enableRule: - return UpdateReturn(enableRule, c => - { - GuardRule.CanEnable(c, Snapshot.RuleDef); - - Enable(c); - - return Snapshot; - }); - case DisableRule disableRule: - return UpdateReturn(disableRule, c => - { - GuardRule.CanDisable(c, Snapshot.RuleDef); - - Disable(c); - - return Snapshot; - }); - case DeleteRule deleteRule: - return Update(deleteRule, c => - { - GuardRule.CanDelete(deleteRule); - - Delete(c); - }); - case TriggerRule triggerRule: - return Trigger(triggerRule); - default: - throw new NotSupportedException(); - } - } - - private async Task Trigger(TriggerRule command) - { - var @event = SimpleMapper.Map(command, new RuleManuallyTriggered { RuleId = Snapshot.Id, AppId = Snapshot.AppId }); - - await ruleEnqueuer.Enqueue(Snapshot.RuleDef, Snapshot.Id, Envelope.Create(@event)); - - return null; - } - - public void Create(CreateRule command) - { - RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); - } - - public void Update(UpdateRule command) - { - RaiseEvent(SimpleMapper.Map(command, new RuleUpdated())); - } - - public void Enable(EnableRule command) - { - RaiseEvent(SimpleMapper.Map(command, new RuleEnabled())); - } - - public void Disable(DisableRule command) - { - RaiseEvent(SimpleMapper.Map(command, new RuleDisabled())); - } - - public void Delete(DeleteRule command) - { - RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); - } - - private void RaiseEvent(AppEvent @event) - { - if (@event.AppId == null) - { - @event.AppId = Snapshot.AppId; - } - - RaiseEvent(Envelope.Create(@event)); - } - - private void VerifyNotDeleted() - { - if (Snapshot.IsDeleted) - { - throw new DomainException("Rule has already been deleted."); - } - } - - public Task> GetStateAsync() - { - return J.AsTask(Snapshot); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs deleted file mode 100644 index 9cd7202b7..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking -{ - public sealed class UsageTrackerCommandMiddleware : ICommandMiddleware - { - private readonly IUsageTrackerGrain usageTrackerGrain; - - public UsageTrackerCommandMiddleware(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - usageTrackerGrain = grainFactory.GetGrain(SingleGrain.Id); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - switch (context.Command) - { - case DeleteRule deleteRule: - await usageTrackerGrain.RemoveTargetAsync(deleteRule.RuleId); - break; - case CreateRule createRule: - { - if (createRule.Trigger is UsageTrigger usage) - { - await usageTrackerGrain.AddTargetAsync(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays); - } - - break; - } - - case UpdateRule ruleUpdated: - { - if (ruleUpdated.Trigger is UsageTrigger usage) - { - await usageTrackerGrain.UpdateTargetAsync(ruleUpdated.RuleId, usage.Limit, usage.NumDays); - } - - break; - } - } - - await next(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs deleted file mode 100644 index 0f7084352..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ /dev/null @@ -1,158 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Orleans; -using Orleans.Concurrency; -using Orleans.Runtime; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; -using Squidex.Infrastructure.UsageTracking; - -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking -{ - [Reentrant] - public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain - { - private readonly IGrainState state; - private readonly IUsageTracker usageTracker; - - public sealed class Target - { - public NamedId AppId { get; set; } - - public int Limits { get; set; } - - public int? NumDays { get; set; } - - public DateTime? Triggered { get; set; } - } - - [CollectionName("UsageTracker")] - public sealed class GrainState - { - public Dictionary Targets { get; set; } = new Dictionary(); - } - - public UsageTrackerGrain(IGrainState state, IUsageTracker usageTracker) - { - Guard.NotNull(state, nameof(state)); - Guard.NotNull(usageTracker, nameof(usageTracker)); - - this.state = state; - - this.usageTracker = usageTracker; - } - - protected override Task OnActivateAsync(string key) - { - DelayDeactivation(TimeSpan.FromDays(1)); - - RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); - RegisterTimer(x => CheckUsagesAsync(), null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); - - return TaskHelper.Done; - } - - public Task ActivateAsync() - { - return TaskHelper.Done; - } - - public Task ReceiveReminder(string reminderName, TickStatus status) - { - return TaskHelper.Done; - } - - public async Task CheckUsagesAsync() - { - var today = DateTime.Today; - - foreach (var kvp in state.Value.Targets) - { - var target = kvp.Value; - - var from = GetFromDate(today, target.NumDays); - - if (!target.Triggered.HasValue || target.Triggered < from) - { - var usage = await usageTracker.GetMonthlyCallsAsync(target.AppId.Id.ToString(), today); - - var limit = kvp.Value.Limits; - - if (usage > limit) - { - kvp.Value.Triggered = today; - - var @event = new AppUsageExceeded - { - AppId = target.AppId, - CallsCurrent = usage, - CallsLimit = limit, - RuleId = kvp.Key - }; - - await state.WriteEventAsync(Envelope.Create(@event)); - } - } - } - - await state.WriteAsync(); - } - - private static DateTime GetFromDate(DateTime today, int? numDays) - { - if (numDays.HasValue) - { - return today.AddDays(-numDays.Value).AddDays(1); - } - else - { - return new DateTime(today.Year, today.Month, 1); - } - } - - public Task AddTargetAsync(Guid ruleId, NamedId appId, int limits, int? numDays) - { - UpdateTarget(ruleId, t => { t.Limits = limits; t.AppId = appId; t.NumDays = numDays; }); - - return state.WriteAsync(); - } - - public Task UpdateTargetAsync(Guid ruleId, int limits, int? numDays) - { - UpdateTarget(ruleId, t => { t.Limits = limits; t.NumDays = numDays; }); - - return state.WriteAsync(); - } - - public Task AddTargetAsync(Guid ruleId, int limits) - { - UpdateTarget(ruleId, t => t.Limits = limits); - - return state.WriteAsync(); - } - - public Task RemoveTargetAsync(Guid ruleId) - { - state.Value.Targets.Remove(ruleId); - - return state.WriteAsync(); - } - - private void UpdateTarget(Guid ruleId, Action updater) - { - updater(state.Value.Targets.GetOrAddNew(ruleId)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs deleted file mode 100644 index 42bcb40e2..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking -{ - public sealed class UsageTriggerHandler : RuleTriggerHandler - { - private const string EventName = "Usage exceeded"; - - protected override Task CreateEnrichedEventAsync(Envelope @event) - { - var result = new EnrichedUsageExceededEvent - { - CallsCurrent = @event.Payload.CallsCurrent, - CallsLimit = @event.Payload.CallsLimit, - Name = EventName - }; - - return Task.FromResult(result); - } - - protected override bool Trigger(EnrichedUsageExceededEvent @event, UsageTrigger trigger) - { - return @event.CallsLimit == trigger.Limit; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs b/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs deleted file mode 100644 index 445218b1d..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Entities.Schemas.Indexes; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public sealed class BackupSchemas : BackupHandler - { - private readonly Dictionary schemasByName = new Dictionary(); - private readonly ISchemasIndex indexSchemas; - - public override string Name { get; } = "Schemas"; - - public BackupSchemas(ISchemasIndex indexSchemas) - { - Guard.NotNull(indexSchemas, nameof(indexSchemas)); - - this.indexSchemas = indexSchemas; - } - - public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) - { - switch (@event.Payload) - { - case SchemaCreated schemaCreated: - schemasByName[schemaCreated.SchemaId.Name] = schemaCreated.SchemaId.Id; - break; - case SchemaDeleted schemaDeleted: - schemasByName.Remove(schemaDeleted.SchemaId.Name); - break; - } - - return TaskHelper.True; - } - - public override Task RestoreAsync(Guid appId, BackupReader reader) - { - return indexSchemas.RebuildAsync(appId, schemasByName); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs deleted file mode 100644 index 92966a7f1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs +++ /dev/null @@ -1,251 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -#pragma warning disable IDE0060 // Remove unused parameter - -namespace Squidex.Domain.Apps.Entities.Schemas.Guards -{ - public static class GuardSchema - { - public static void CanCreate(CreateSchema command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot create schema.", e => - { - if (!command.Name.IsSlug()) - { - e(Not.ValidSlug("Name"), nameof(command.Name)); - } - - ValidateUpsert(command, e); - }); - } - - public static void CanSynchronize(SynchronizeSchema command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot synchronize schema.", e => - { - ValidateUpsert(command, e); - }); - } - - public static void CanReorder(Schema schema, ReorderFields command) - { - Guard.NotNull(command, nameof(command)); - - IArrayField arrayField = null; - - if (command.ParentFieldId.HasValue) - { - arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false); - } - - Validate.It(() => "Cannot reorder schema fields.", error => - { - if (command.FieldIds == null) - { - error("Field ids is required.", nameof(command.FieldIds)); - } - - if (arrayField == null) - { - ValidateFieldIds(error, command, schema.FieldsById); - } - else - { - ValidateFieldIds(error, command, arrayField.FieldsById); - } - }); - } - - public static void CanConfigurePreviewUrls(ConfigurePreviewUrls command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot configure preview urls.", error => - { - if (command.PreviewUrls == null) - { - error("Preview Urls is required.", nameof(command.PreviewUrls)); - } - }); - } - - public static void CanPublish(Schema schema, PublishSchema command) - { - Guard.NotNull(command, nameof(command)); - - if (schema.IsPublished) - { - throw new DomainException("Schema is already published."); - } - } - - public static void CanUnpublish(Schema schema, UnpublishSchema command) - { - Guard.NotNull(command, nameof(command)); - - if (!schema.IsPublished) - { - throw new DomainException("Schema is not published."); - } - } - - public static void CanUpdate(Schema schema, UpdateSchema command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanConfigureScripts(Schema schema, ConfigureScripts command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanChangeCategory(Schema schema, ChangeCategory command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanDelete(Schema schema, DeleteSchema command) - { - Guard.NotNull(command, nameof(command)); - } - - private static void ValidateUpsert(UpsertCommand command, AddValidation e) - { - if (command.Fields?.Count > 0) - { - var fieldIndex = 0; - var fieldPrefix = string.Empty; - - foreach (var field in command.Fields) - { - fieldIndex++; - fieldPrefix = $"Fields[{fieldIndex}]"; - - ValidateRootField(field, fieldPrefix, e); - } - - if (command.Fields.Select(x => x?.Name).Distinct().Count() != command.Fields.Count) - { - e("Fields cannot have duplicate names.", nameof(command.Fields)); - } - } - } - - private static void ValidateRootField(UpsertSchemaField field, string prefix, AddValidation e) - { - if (field == null) - { - e(Not.Defined("Field"), prefix); - } - else - { - if (!field.Partitioning.IsValidPartitioning()) - { - e(Not.Valid("Partitioning"), $"{prefix}.{nameof(field.Partitioning)}"); - } - - ValidateField(field, prefix, e); - - if (field.Nested?.Count > 0) - { - if (field.Properties is ArrayFieldProperties) - { - var nestedIndex = 0; - var nestedPrefix = string.Empty; - - foreach (var nestedField in field.Nested) - { - nestedIndex++; - nestedPrefix = $"{prefix}.Nested[{nestedIndex}]"; - - ValidateNestedField(nestedField, nestedPrefix, e); - } - } - else if (field.Nested.Count > 0) - { - e("Only array fields can have nested fields.", $"{prefix}.{nameof(field.Partitioning)}"); - } - - if (field.Nested.Select(x => x.Name).Distinct().Count() != field.Nested.Count) - { - e("Fields cannot have duplicate names.", $"{prefix}.Nested"); - } - } - } - } - - private static void ValidateNestedField(UpsertSchemaNestedField nestedField, string prefix, AddValidation e) - { - if (nestedField == null) - { - e(Not.Defined("Field"), prefix); - } - else - { - if (nestedField.Properties is ArrayFieldProperties) - { - e("Nested field cannot be array fields.", $"{prefix}.{nameof(nestedField.Properties)}"); - } - - ValidateField(nestedField, prefix, e); - } - } - - private static void ValidateField(UpsertSchemaFieldBase field, string prefix, AddValidation e) - { - if (!field.Name.IsPropertyName()) - { - e("Field name must be a valid javascript property name.", $"{prefix}.{nameof(field.Name)}"); - } - - if (field.Properties == null) - { - e(Not.Defined("Field properties"), $"{prefix}.{nameof(field.Properties)}"); - } - else - { - if (!field.Properties.IsForApi()) - { - if (field.IsHidden) - { - e("UI field cannot be hidden.", $"{prefix}.{nameof(field.IsHidden)}"); - } - - if (field.IsDisabled) - { - e("UI field cannot be disabled.", $"{prefix}.{nameof(field.IsDisabled)}"); - } - } - - var errors = FieldPropertiesValidator.Validate(field.Properties); - - errors.Foreach(x => x.WithPrefix($"{prefix}.{nameof(field.Properties)}").AddTo(e)); - } - } - - private static void ValidateFieldIds(AddValidation error, ReorderFields c, IReadOnlyDictionary fields) - { - if (c.FieldIds != null && (c.FieldIds.Count != fields.Count || c.FieldIds.Any(x => !fields.ContainsKey(x)))) - { - error("Field ids do not cover all fields.", nameof(c.FieldIds)); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs deleted file mode 100644 index 46d5fbd8e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs +++ /dev/null @@ -1,167 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Schemas.Guards -{ - public static class GuardSchemaField - { - public static void CanAdd(Schema schema, AddField command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot add a new field.", e => - { - if (!command.Name.IsPropertyName()) - { - e("Name must be a valid javascript property name.", nameof(command.Name)); - } - - if (command.Properties == null) - { - e(Not.Defined("Properties"), nameof(command.Properties)); - } - else - { - var errors = FieldPropertiesValidator.Validate(command.Properties); - - errors.Foreach(x => x.WithPrefix(nameof(command.Properties)).AddTo(e)); - } - - if (command.ParentFieldId.HasValue) - { - var arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false); - - if (arrayField.FieldsByName.ContainsKey(command.Name)) - { - e("A field with the same name already exists."); - } - } - else - { - if (command.ParentFieldId == null && !command.Partitioning.IsValidPartitioning()) - { - e(Not.Valid("Partitioning"), nameof(command.Partitioning)); - } - - if (schema.FieldsByName.ContainsKey(command.Name)) - { - e("A field with the same name already exists."); - } - } - }); - } - - public static void CanUpdate(Schema schema, UpdateField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - Validate.It(() => "Cannot update field.", e => - { - if (command.Properties == null) - { - e(Not.Defined("Properties"), nameof(command.Properties)); - } - else - { - var errors = FieldPropertiesValidator.Validate(command.Properties); - - errors.Foreach(x => x.WithPrefix(nameof(command.Properties)).AddTo(e)); - } - }); - } - - public static void CanHide(Schema schema, HideField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - if (field.IsHidden) - { - throw new DomainException("Schema field is already hidden."); - } - - if (!field.IsForApi()) - { - throw new DomainException("UI field cannot be hidden."); - } - } - - public static void CanShow(Schema schema, ShowField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - if (!field.IsHidden) - { - throw new DomainException("Schema field is already visible."); - } - } - - public static void CanDisable(Schema schema, DisableField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - if (field.IsDisabled) - { - throw new DomainException("Schema field is already disabled."); - } - - if (!field.IsForApi(true)) - { - throw new DomainException("UI field cannot be disabled."); - } - } - - public static void CanDelete(Schema schema, DeleteField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - if (field.IsLocked) - { - throw new DomainException("Schema field is locked."); - } - } - - public static void CanEnable(Schema schema, EnableField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - if (!field.IsDisabled) - { - throw new DomainException("Schema field is already enabled."); - } - } - - public static void CanLock(Schema schema, LockField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - if (field.IsLocked) - { - throw new DomainException("Schema field is already locked."); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs deleted file mode 100644 index f750b9935..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes -{ - public interface ISchemasIndex - { - Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false); - - Task GetSchemaByNameAsync(Guid appId, string name, bool allowDeleted = false); - - Task> GetSchemasAsync(Guid appId, bool allowDeleted = false); - - Task RebuildAsync(Guid appId, Dictionary schemas); - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs deleted file mode 100644 index 6a09a538f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs +++ /dev/null @@ -1,181 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes -{ - public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex - { - private readonly IGrainFactory grainFactory; - - public SchemasIndex(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public Task RebuildAsync(Guid appId, Dictionary schemas) - { - return Index(appId).RebuildAsync(schemas); - } - - public async Task> GetSchemasAsync(Guid appId, bool allowDeleted = false) - { - using (Profiler.TraceMethod()) - { - var ids = await GetSchemaIdsAsync(appId); - - var schemas = - await Task.WhenAll( - ids.Select(id => GetSchemaAsync(appId, id, allowDeleted))); - - return schemas.Where(x => x != null).ToList(); - } - } - - public async Task GetSchemaByNameAsync(Guid appId, string name, bool allowDeleted = false) - { - using (Profiler.TraceMethod()) - { - var id = await GetSchemaIdAsync(appId, name); - - if (id == default) - { - return null; - } - - return await GetSchemaAsync(appId, id, allowDeleted); - } - } - - public async Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) - { - using (Profiler.TraceMethod()) - { - var schema = await grainFactory.GetGrain(id).GetStateAsync(); - - if (IsFound(schema.Value, allowDeleted)) - { - return schema.Value; - } - - return null; - } - } - - private async Task GetSchemaIdAsync(Guid appId, string name) - { - using (Profiler.TraceMethod()) - { - return await Index(appId).GetIdAsync(name); - } - } - - private async Task> GetSchemaIdsAsync(Guid appId) - { - using (Profiler.TraceMethod()) - { - return await Index(appId).GetIdsAsync(); - } - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (context.Command is CreateSchema createSchema) - { - var index = Index(createSchema.AppId.Id); - - string token = await CheckSchemaAsync(index, createSchema); - - try - { - await next(); - } - finally - { - if (token != null) - { - if (context.IsCompleted) - { - await index.AddAsync(token); - } - else - { - await index.RemoveReservationAsync(token); - } - } - } - } - else - { - await next(); - - if (context.IsCompleted) - { - if (context.Command is DeleteSchema deleteSchema) - { - await DeleteSchemaAsync(deleteSchema); - } - } - } - } - - private async Task CheckSchemaAsync(ISchemasByAppIndexGrain index, CreateSchema command) - { - var name = command.Name; - - if (name.IsSlug()) - { - var token = await index.ReserveAsync(command.SchemaId, name); - - if (token == null) - { - var error = new ValidationError("A schema with this name already exists."); - - throw new ValidationException("Cannot create schema.", error); - } - - return token; - } - - return null; - } - - private async Task DeleteSchemaAsync(DeleteSchema commmand) - { - var schemaId = commmand.SchemaId; - - var schema = await grainFactory.GetGrain(schemaId).GetStateAsync(); - - if (IsFound(schema.Value, true)) - { - await Index(schema.Value.AppId.Id).RemoveAsync(schemaId); - } - } - - private ISchemasByAppIndexGrain Index(Guid appId) - { - return grainFactory.GetGrain(appId); - } - - private static bool IsFound(ISchemaEntity entity, bool allowDeleted) - { - return entity.Version > EtagVersion.Empty && (!entity.IsDeleted || allowDeleted); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs deleted file mode 100644 index 46dd6c0e7..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public sealed class SchemaChangedTriggerHandler : RuleTriggerHandler - { - private readonly IScriptEngine scriptEngine; - - public SchemaChangedTriggerHandler(IScriptEngine scriptEngine) - { - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - - this.scriptEngine = scriptEngine; - } - - protected override Task CreateEnrichedEventAsync(Envelope @event) - { - var result = new EnrichedSchemaEvent(); - - SimpleMapper.Map(@event.Payload, result); - - switch (@event.Payload) - { - case FieldEvent _: - case SchemaPreviewUrlsConfigured _: - case SchemaScriptsConfigured _: - case SchemaUpdated _: - case ParentFieldEvent _: - result.Type = EnrichedSchemaEventType.Updated; - break; - case SchemaCreated _: - result.Type = EnrichedSchemaEventType.Created; - break; - case SchemaPublished _: - result.Type = EnrichedSchemaEventType.Published; - break; - case SchemaUnpublished _: - result.Type = EnrichedSchemaEventType.Unpublished; - break; - case SchemaDeleted _: - result.Type = EnrichedSchemaEventType.Deleted; - break; - default: - result = null; - break; - } - - result.Name = $"Schema{result.Type}"; - - return Task.FromResult(result); - } - - protected override bool Trigger(EnrichedSchemaEvent @event, SchemaChangedTrigger trigger) - { - return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs deleted file mode 100644 index 1689b4cd1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs +++ /dev/null @@ -1,417 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.EventSynchronization; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.Schemas.Guards; -using Squidex.Domain.Apps.Entities.Schemas.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public sealed class SchemaGrain : DomainObjectGrain, ISchemaGrain - { - private readonly IJsonSerializer serializer; - - public SchemaGrain(IStore store, ISemanticLog log, IJsonSerializer serializer) - : base(store, log) - { - Guard.NotNull(serializer, nameof(serializer)); - - this.serializer = serializer; - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - VerifyNotDeleted(); - - switch (command) - { - case AddField addField: - return UpdateReturn(addField, c => - { - GuardSchemaField.CanAdd(Snapshot.SchemaDef, c); - - Add(c); - - long id; - - if (c.ParentFieldId == null) - { - id = Snapshot.SchemaDef.FieldsByName[c.Name].Id; - } - else - { - id = ((IArrayField)Snapshot.SchemaDef.FieldsById[c.ParentFieldId.Value]).FieldsByName[c.Name].Id; - } - - return Snapshot; - }); - - case CreateSchema createSchema: - return CreateReturn(createSchema, c => - { - GuardSchema.CanCreate(c); - - Create(c); - - return Snapshot; - }); - - case SynchronizeSchema synchronizeSchema: - return UpdateReturn(synchronizeSchema, c => - { - GuardSchema.CanSynchronize(c); - - Synchronize(c); - - return Snapshot; - }); - - case DeleteField deleteField: - return UpdateReturn(deleteField, c => - { - GuardSchemaField.CanDelete(Snapshot.SchemaDef, deleteField); - - DeleteField(c); - - return Snapshot; - }); - - case LockField lockField: - return UpdateReturn(lockField, c => - { - GuardSchemaField.CanLock(Snapshot.SchemaDef, lockField); - - LockField(c); - - return Snapshot; - }); - - case HideField hideField: - return UpdateReturn(hideField, c => - { - GuardSchemaField.CanHide(Snapshot.SchemaDef, c); - - HideField(c); - - return Snapshot; - }); - - case ShowField showField: - return UpdateReturn(showField, c => - { - GuardSchemaField.CanShow(Snapshot.SchemaDef, c); - - ShowField(c); - - return Snapshot; - }); - - case DisableField disableField: - return UpdateReturn(disableField, c => - { - GuardSchemaField.CanDisable(Snapshot.SchemaDef, c); - - DisableField(c); - - return Snapshot; - }); - - case EnableField enableField: - return UpdateReturn(enableField, c => - { - GuardSchemaField.CanEnable(Snapshot.SchemaDef, c); - - EnableField(c); - - return Snapshot; - }); - - case UpdateField updateField: - return UpdateReturn(updateField, c => - { - GuardSchemaField.CanUpdate(Snapshot.SchemaDef, c); - - UpdateField(c); - - return Snapshot; - }); - - case ReorderFields reorderFields: - return UpdateReturn(reorderFields, c => - { - GuardSchema.CanReorder(Snapshot.SchemaDef, c); - - Reorder(c); - - return Snapshot; - }); - - case UpdateSchema updateSchema: - return UpdateReturn(updateSchema, c => - { - GuardSchema.CanUpdate(Snapshot.SchemaDef, c); - - Update(c); - - return Snapshot; - }); - - case PublishSchema publishSchema: - return UpdateReturn(publishSchema, c => - { - GuardSchema.CanPublish(Snapshot.SchemaDef, c); - - Publish(c); - - return Snapshot; - }); - - case UnpublishSchema unpublishSchema: - return UpdateReturn(unpublishSchema, c => - { - GuardSchema.CanUnpublish(Snapshot.SchemaDef, c); - - Unpublish(c); - - return Snapshot; - }); - - case ConfigureScripts configureScripts: - return UpdateReturn(configureScripts, c => - { - GuardSchema.CanConfigureScripts(Snapshot.SchemaDef, c); - - ConfigureScripts(c); - - return Snapshot; - }); - - case ChangeCategory changeCategory: - return UpdateReturn(changeCategory, c => - { - GuardSchema.CanChangeCategory(Snapshot.SchemaDef, c); - - ChangeCategory(c); - - return Snapshot; - }); - - case ConfigurePreviewUrls configurePreviewUrls: - return UpdateReturn(configurePreviewUrls, c => - { - GuardSchema.CanConfigurePreviewUrls(c); - - ConfigurePreviewUrls(c); - - return Snapshot; - }); - - case DeleteSchema deleteSchema: - return Update(deleteSchema, c => - { - GuardSchema.CanDelete(Snapshot.SchemaDef, c); - - Delete(c); - }); - - default: - throw new NotSupportedException(); - } - } - - public void Synchronize(SynchronizeSchema command) - { - var options = new SchemaSynchronizationOptions - { - NoFieldDeletion = command.NoFieldDeletion, - NoFieldRecreation = command.NoFieldRecreation - }; - - var schemaSource = Snapshot.SchemaDef; - var schemaTarget = command.ToSchema(schemaSource.Name, schemaSource.IsSingleton); - - var events = schemaSource.Synchronize(schemaTarget, serializer, () => Snapshot.SchemaFieldsTotal + 1, options); - - foreach (var @event in events) - { - RaiseEvent(SimpleMapper.Map(command, (SchemaEvent)@event)); - } - } - - public void Create(CreateSchema command) - { - RaiseEvent(command, new SchemaCreated { SchemaId = NamedId.Of(command.SchemaId, command.Name), Schema = command.ToSchema() }); - } - - public void Add(AddField command) - { - RaiseEvent(command, new FieldAdded { FieldId = CreateFieldId(command) }); - } - - public void UpdateField(UpdateField command) - { - RaiseEvent(command, new FieldUpdated()); - } - - public void LockField(LockField command) - { - RaiseEvent(command, new FieldLocked()); - } - - public void HideField(HideField command) - { - RaiseEvent(command, new FieldHidden()); - } - - public void ShowField(ShowField command) - { - RaiseEvent(command, new FieldShown()); - } - - public void DisableField(DisableField command) - { - RaiseEvent(command, new FieldDisabled()); - } - - public void EnableField(EnableField command) - { - RaiseEvent(command, new FieldEnabled()); - } - - public void DeleteField(DeleteField command) - { - RaiseEvent(command, new FieldDeleted()); - } - - public void Reorder(ReorderFields command) - { - RaiseEvent(command, new SchemaFieldsReordered()); - } - - public void Publish(PublishSchema command) - { - RaiseEvent(command, new SchemaPublished()); - } - - public void Unpublish(UnpublishSchema command) - { - RaiseEvent(command, new SchemaUnpublished()); - } - - public void ConfigureScripts(ConfigureScripts command) - { - RaiseEvent(command, new SchemaScriptsConfigured()); - } - - public void ChangeCategory(ChangeCategory command) - { - RaiseEvent(command, new SchemaCategoryChanged()); - } - - public void ConfigurePreviewUrls(ConfigurePreviewUrls command) - { - RaiseEvent(command, new SchemaPreviewUrlsConfigured()); - } - - public void Update(UpdateSchema command) - { - RaiseEvent(command, new SchemaUpdated()); - } - - public void Delete(DeleteSchema command) - { - RaiseEvent(command, new SchemaDeleted()); - } - - private void RaiseEvent(TCommand command, TEvent @event) where TCommand : SchemaCommand where TEvent : SchemaEvent - { - SimpleMapper.Map(command, @event); - - NamedId GetFieldId(long? id) - { - if (id.HasValue && Snapshot.SchemaDef.FieldsById.TryGetValue(id.Value, out var field)) - { - return field.NamedId(); - } - - return null; - } - - if (command is ParentFieldCommand pc && @event is ParentFieldEvent pe) - { - if (pc.ParentFieldId.HasValue) - { - if (Snapshot.SchemaDef.FieldsById.TryGetValue(pc.ParentFieldId.Value, out var field)) - { - pe.ParentFieldId = field.NamedId(); - - if (command is FieldCommand fc && @event is FieldEvent fe) - { - if (field is IArrayField arrayField && arrayField.FieldsById.TryGetValue(fc.FieldId, out var nestedField)) - { - fe.FieldId = nestedField.NamedId(); - } - } - } - } - else if (command is FieldCommand fc && @event is FieldEvent fe) - { - fe.FieldId = GetFieldId(fc.FieldId); - } - } - - RaiseEvent(@event); - } - - private void RaiseEvent(SchemaEvent @event) - { - if (@event.SchemaId == null) - { - @event.SchemaId = Snapshot.NamedId(); - } - - if (@event.AppId == null) - { - @event.AppId = Snapshot.AppId; - } - - RaiseEvent(Envelope.Create(@event)); - } - - private NamedId CreateFieldId(AddField command) - { - return NamedId.Of(Snapshot.SchemaFieldsTotal + 1, command.Name); - } - - private void VerifyNotDeleted() - { - if (Snapshot.IsDeleted) - { - throw new DomainException("Schema has already been deleted."); - } - } - - public Task> GetStateAsync() - { - return J.AsTask(Snapshot); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs deleted file mode 100644 index d452f452c..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.History; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public sealed class SchemaHistoryEventsCreator : HistoryEventsCreatorBase - { - public SchemaHistoryEventsCreator(TypeNameRegistry typeNameRegistry) - : base(typeNameRegistry) - { - AddEventMessage( - "reordered fields of schema {[Name]}."); - - AddEventMessage( - "created schema {[Name]}."); - - AddEventMessage( - "updated schema {[Name]}."); - - AddEventMessage( - "deleted schema {[Name]}."); - - AddEventMessage( - "published schema {[Name]}."); - - AddEventMessage( - "unpublished schema {[Name]}."); - - AddEventMessage( - "reordered fields of schema {[Name]}."); - - AddEventMessage( - "configured script of schema {[Name]}."); - - AddEventMessage( - "added field {[Field]} to schema {[Name]}."); - - AddEventMessage( - "deleted field {[Field]} from schema {[Name]}."); - - AddEventMessage( - "has locked field {[Field]} of schema {[Name]}."); - - AddEventMessage( - "has hidden field {[Field]} of schema {[Name]}."); - - AddEventMessage( - "has shown field {[Field]} of schema {[Name]}."); - - AddEventMessage( - "disabled field {[Field]} of schema {[Name]}."); - - AddEventMessage( - "disabled field {[Field]} of schema {[Name]}."); - - AddEventMessage( - "has updated field {[Field]} of schema {[Name]}."); - - AddEventMessage( - "deleted field {[Field]} of schema {[Name]}."); - } - - protected override Task CreateEventCoreAsync(Envelope @event) - { - if (@event.Payload is SchemaEvent schemaEvent) - { - var channel = $"schemas.{schemaEvent.SchemaId.Name}"; - - var result = ForEvent(@event.Payload, channel).Param("Name", schemaEvent.SchemaId.Name); - - if (schemaEvent is FieldEvent fieldEvent) - { - result.Param("Field", fieldEvent.FieldId.Name); - } - - return Task.FromResult(result); - } - - return Task.FromResult(null); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj deleted file mode 100644 index e92f011f5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs b/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs deleted file mode 100644 index 08f1ff835..000000000 --- a/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Tags -{ - public sealed class GrainTagService : ITagService - { - private readonly IGrainFactory grainFactory; - - public string Name - { - get { return "Tags"; } - } - - public GrainTagService(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public Task> NormalizeTagsAsync(Guid appId, string group, HashSet names, HashSet ids) - { - return GetGrain(appId, group).NormalizeTagsAsync(names, ids); - } - - public Task> GetTagIdsAsync(Guid appId, string group, HashSet names) - { - return GetGrain(appId, group).GetTagIdsAsync(names); - } - - public Task> DenormalizeTagsAsync(Guid appId, string group, HashSet ids) - { - return GetGrain(appId, group).DenormalizeTagsAsync(ids); - } - - public Task GetTagsAsync(Guid appId, string group) - { - return GetGrain(appId, group).GetTagsAsync(); - } - - public Task GetExportableTagsAsync(Guid appId, string group) - { - return GetGrain(appId, group).GetExportableTagsAsync(); - } - - public Task RebuildTagsAsync(Guid appId, string group, TagsExport tags) - { - return GetGrain(appId, group).RebuildAsync(tags); - } - - public Task ClearAsync(Guid appId, string group) - { - return GetGrain(appId, group).ClearAsync(); - } - - private ITagGrain GetGrain(Guid appId, string group) - { - Guard.NotNullOrEmpty(group, nameof(group)); - - return grainFactory.GetGrain($"{appId}_{group}"); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs b/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs deleted file mode 100644 index be9a5bdfb..000000000 --- a/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// 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 Orleans; -using Squidex.Domain.Apps.Core.Tags; - -namespace Squidex.Domain.Apps.Entities.Tags -{ - public interface ITagGrain : IGrainWithStringKey - { - Task> NormalizeTagsAsync(HashSet names, HashSet ids); - - Task> GetTagIdsAsync(HashSet names); - - Task> DenormalizeTagsAsync(HashSet ids); - - Task GetTagsAsync(); - - Task GetExportableTagsAsync(); - - Task ClearAsync(); - - Task RebuildAsync(TagsExport tags); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs b/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs deleted file mode 100644 index 571316e20..000000000 --- a/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs +++ /dev/null @@ -1,152 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Tags -{ - public sealed class TagGrain : GrainOfString, ITagGrain - { - private readonly IGrainState state; - - [CollectionName("Index_Tags")] - public sealed class GrainState - { - public TagsExport Tags { get; set; } = new TagsExport(); - } - - public TagGrain(IGrainState state) - { - Guard.NotNull(state, nameof(state)); - - this.state = state; - } - - public Task ClearAsync() - { - return state.ClearAsync(); - } - - public Task RebuildAsync(TagsExport tags) - { - state.Value.Tags = tags; - - return state.WriteAsync(); - } - - public async Task> NormalizeTagsAsync(HashSet names, HashSet ids) - { - var result = new Dictionary(); - - if (names != null) - { - foreach (var tag in names) - { - if (!string.IsNullOrWhiteSpace(tag)) - { - var tagName = tag.ToLowerInvariant(); - var tagId = string.Empty; - - var found = state.Value.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, tagName, StringComparison.OrdinalIgnoreCase)); - - if (found.Value != null) - { - tagId = found.Key; - - if (ids == null || !ids.Contains(tagId)) - { - found.Value.Count++; - } - } - else - { - tagId = Guid.NewGuid().ToString(); - - state.Value.Tags.Add(tagId, new Tag { Name = tagName }); - } - - result.Add(tagName, tagId); - } - } - } - - if (ids != null) - { - foreach (var id in ids) - { - if (!result.ContainsValue(id)) - { - if (state.Value.Tags.TryGetValue(id, out var tagInfo)) - { - tagInfo.Count--; - - if (tagInfo.Count <= 0) - { - state.Value.Tags.Remove(id); - } - } - } - } - } - - await state.WriteAsync(); - - return result; - } - - public Task> GetTagIdsAsync(HashSet names) - { - var result = new Dictionary(); - - foreach (var name in names) - { - var id = state.Value.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, name, StringComparison.OrdinalIgnoreCase)).Key; - - if (!string.IsNullOrWhiteSpace(id)) - { - result.Add(name, id); - } - } - - return Task.FromResult(result); - } - - public Task> DenormalizeTagsAsync(HashSet ids) - { - var result = new Dictionary(); - - foreach (var id in ids) - { - if (state.Value.Tags.TryGetValue(id, out var tagInfo)) - { - result[id] = tagInfo.Name; - } - } - - return Task.FromResult(result); - } - - public Task GetTagsAsync() - { - var tags = state.Value.Tags.Values.ToDictionary(x => x.Name, x => x.Count); - - return Task.FromResult(new TagsSet(tags, state.Version)); - } - - public Task GetExportableTagsAsync() - { - return Task.FromResult(state.Value.Tags); - } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs b/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs deleted file mode 100644 index 4075b82f6..000000000 --- a/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Events.Apps -{ - [EventType(nameof(AppPatternAdded))] - public sealed class AppPatternAdded : AppEvent - { - public Guid PatternId { get; set; } - - public string Name { get; set; } - - public string Pattern { get; set; } - - public string Message { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs b/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs deleted file mode 100644 index 81a9c6c39..000000000 --- a/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Events.Apps -{ - [EventType(nameof(AppPatternUpdated))] - public sealed class AppPatternUpdated : AppEvent - { - public Guid PatternId { get; set; } - - public string Name { get; set; } - - public string Pattern { get; set; } - - public string Message { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs b/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs deleted file mode 100644 index d2cec90a8..000000000 --- a/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Events.Assets -{ - [EventType(nameof(AssetAnnotated))] - public sealed class AssetAnnotated : AssetEvent - { - public string FileName { get; set; } - - public string Slug { get; set; } - - public HashSet Tags { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs b/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs deleted file mode 100644 index 5200031cc..000000000 --- a/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Events.Assets -{ - [EventType(nameof(AssetCreated))] - public sealed class AssetCreated : AssetEvent - { - public string FileName { get; set; } - - public string FileHash { get; set; } - - public string MimeType { get; set; } - - public string Slug { get; set; } - - public long FileVersion { get; set; } - - public long FileSize { get; set; } - - public bool IsImage { get; set; } - - public int? PixelWidth { get; set; } - - public int? PixelHeight { get; set; } - - public HashSet Tags { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs b/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs deleted file mode 100644 index d73358c30..000000000 --- a/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Events.Schemas -{ - [EventType(nameof(FieldAdded))] - public sealed class FieldAdded : FieldEvent - { - public string Name { get; set; } - - public string Partitioning { get; set; } - - public FieldProperties Properties { get; set; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs b/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs deleted file mode 100644 index 0406c3c40..000000000 --- a/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Events.Schemas -{ - public abstract class ParentFieldEvent : SchemaEvent - { - public NamedId ParentFieldId { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj deleted file mode 100644 index b0cf01c94..000000000 --- a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUser.cs b/src/Squidex.Domain.Users.MongoDb/MongoUser.cs deleted file mode 100644 index f3a7a1876..000000000 --- a/src/Squidex.Domain.Users.MongoDb/MongoUser.cs +++ /dev/null @@ -1,99 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using Microsoft.AspNetCore.Identity; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Users.MongoDb -{ - public sealed class MongoUser : IdentityUser - { - public List Claims { get; set; } = new List(); - - public List Tokens { get; set; } = new List(); - - public List Logins { get; set; } = new List(); - - public HashSet Roles { get; set; } = new HashSet(); - - internal void AddLogin(UserLoginInfo login) - { - Logins.Add(new UserLoginInfo(login.LoginProvider, login.ProviderKey, login.ProviderDisplayName)); - } - - internal void AddRole(string role) - { - Roles.Add(role); - } - - internal void RemoveRole(string role) - { - Roles.Remove(role); - } - - internal void RemoveLogin(string loginProvider, string providerKey) - { - Logins.RemoveAll(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); - } - - internal void AddClaim(Claim claim) - { - Claims.Add(claim); - } - - internal void AddClaims(IEnumerable claims) - { - claims.Foreach(AddClaim); - } - - internal void RemoveClaim(Claim claim) - { - Claims.RemoveAll(c => c.Type == claim.Type && c.Value == claim.Value); - } - - internal void RemoveClaims(IEnumerable claims) - { - claims.Foreach(RemoveClaim); - } - - internal string GetToken(string loginProvider, string name) - { - return Tokens.FirstOrDefault(t => t.LoginProvider == loginProvider && t.Name == name)?.Value; - } - - internal void AddToken(string loginProvider, string name, string value) - { - Tokens.Add(new UserTokenInfo { LoginProvider = loginProvider, Name = name, Value = value }); - } - - internal void RemoveToken(string loginProvider, string name) - { - Tokens.RemoveAll(t => t.LoginProvider == loginProvider && t.Name == name); - } - - internal void ReplaceClaim(Claim existingClaim, Claim newClaim) - { - RemoveClaim(existingClaim); - - AddClaim(newClaim); - } - - internal void SetToken(string loginProider, string name, string value) - { - RemoveToken(loginProider, name); - - AddToken(loginProider, name, value); - } - } - - public sealed class UserTokenInfo : IdentityUserToken - { - } -} diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs b/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs deleted file mode 100644 index 30f29c2c0..000000000 --- a/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs +++ /dev/null @@ -1,526 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; -using MongoDB.Driver; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Users.MongoDb -{ - public sealed class MongoUserStore : - MongoRepositoryBase, - IUserAuthenticationTokenStore, - IUserAuthenticatorKeyStore, - IUserClaimStore, - IUserEmailStore, - IUserFactory, - IUserLockoutStore, - IUserLoginStore, - IUserPasswordStore, - IUserPhoneNumberStore, - IUserRoleStore, - IUserSecurityStampStore, - IUserTwoFactorStore, - IUserTwoFactorRecoveryCodeStore, - IQueryableUserStore - { - private const string InternalLoginProvider = "[AspNetUserStore]"; - private const string AuthenticatorKeyTokenName = "AuthenticatorKey"; - private const string RecoveryCodeTokenName = "RecoveryCodes"; - - static MongoUserStore() - { - BsonClassMap.RegisterClassMap(cm => - { - cm.MapConstructor(typeof(Claim).GetConstructors() - .First(x => - { - var parameters = x.GetParameters(); - - return parameters.Length == 2 && - parameters[0].Name == "type" && - parameters[0].ParameterType == typeof(string) && - parameters[1].Name == "value" && - parameters[1].ParameterType == typeof(string); - })) - .SetArguments(new[] - { - nameof(Claim.Type), - nameof(Claim.Value) - }); - - cm.MapMember(x => x.Type); - cm.MapMember(x => x.Value); - }); - - BsonClassMap.RegisterClassMap(cm => - { - cm.MapConstructor(typeof(UserLoginInfo).GetConstructors().First()) - .SetArguments(new[] - { - nameof(UserLoginInfo.LoginProvider), - nameof(UserLoginInfo.ProviderKey), - nameof(UserLoginInfo.ProviderDisplayName) - }); - - cm.AutoMap(); - }); - - BsonClassMap.RegisterClassMap>(cm => - { - cm.AutoMap(); - - cm.UnmapMember(x => x.UserId); - }); - - BsonClassMap.RegisterClassMap>(cm => - { - cm.AutoMap(); - - cm.MapMember(x => x.Id).SetSerializer(new StringSerializer(BsonType.ObjectId)); - cm.MapMember(x => x.AccessFailedCount).SetIgnoreIfDefault(true); - cm.MapMember(x => x.EmailConfirmed).SetIgnoreIfDefault(true); - cm.MapMember(x => x.LockoutEnd).SetElementName("LockoutEndDateUtc").SetIgnoreIfNull(true); - cm.MapMember(x => x.LockoutEnabled).SetIgnoreIfDefault(true); - cm.MapMember(x => x.PasswordHash).SetIgnoreIfNull(true); - cm.MapMember(x => x.PhoneNumber).SetIgnoreIfNull(true); - cm.MapMember(x => x.PhoneNumberConfirmed).SetIgnoreIfDefault(true); - cm.MapMember(x => x.SecurityStamp).SetIgnoreIfNull(true); - cm.MapMember(x => x.TwoFactorEnabled).SetIgnoreIfDefault(true); - }); - } - - public MongoUserStore(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "Identity_Users"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index - .Ascending("Logins.LoginProvider") - .Ascending("Logins.ProviderKey")), - new CreateIndexModel( - Index - .Ascending(x => x.NormalizedUserName), - new CreateIndexOptions - { - Unique = true - }), - new CreateIndexModel( - Index - .Ascending(x => x.NormalizedEmail), - new CreateIndexOptions - { - Unique = true - }) - }, ct); - } - - protected override MongoCollectionSettings CollectionSettings() - { - return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; - } - - public void Dispose() - { - } - - public IQueryable Users - { - get { return Collection.AsQueryable(); } - } - - public bool IsId(string id) - { - return ObjectId.TryParse(id, out _); - } - - public IdentityUser Create(string email) - { - return new MongoUser { Email = email, UserName = email }; - } - - public async Task FindByIdAsync(string userId, CancellationToken cancellationToken) - { - if (!IsId(userId)) - { - return null; - } - - return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken); - } - - public async Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) - { - return await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken); - } - - public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) - { - return await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken); - } - - public async Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) - { - return await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken); - } - - public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) - { - return (await Collection.Find(x => x.Claims.Any(y => y.Type == claim.Type && y.Value == claim.Value)).ToListAsync(cancellationToken)).OfType().ToList(); - } - - public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) - { - return (await Collection.Find(x => x.Roles.Contains(roleName)).ToListAsync(cancellationToken)).OfType().ToList(); - } - - public async Task CreateAsync(IdentityUser user, CancellationToken cancellationToken) - { - user.Id = ObjectId.GenerateNewId().ToString(); - - await Collection.InsertOneAsync((MongoUser)user, null, cancellationToken); - - return IdentityResult.Success; - } - - public async Task UpdateAsync(IdentityUser user, CancellationToken cancellationToken) - { - await Collection.ReplaceOneAsync(x => x.Id == user.Id, (MongoUser)user, null, cancellationToken); - - return IdentityResult.Success; - } - - public async Task DeleteAsync(IdentityUser user, CancellationToken cancellationToken) - { - await Collection.DeleteOneAsync(x => x.Id == user.Id, null, cancellationToken); - - return IdentityResult.Success; - } - - public Task GetUserIdAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).Id); - } - - public Task GetUserNameAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).UserName); - } - - public Task GetNormalizedUserNameAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).NormalizedUserName); - } - - public Task GetPasswordHashAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).PasswordHash); - } - - public Task> GetRolesAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult>(((MongoUser)user).Roles.ToList()); - } - - public Task IsInRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).Roles.Contains(roleName)); - } - - public Task> GetLoginsAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult>(((MongoUser)user).Logins.Select(x => new UserLoginInfo(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList()); - } - - public Task GetSecurityStampAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).SecurityStamp); - } - - public Task GetEmailAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).Email); - } - - public Task GetEmailConfirmedAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).EmailConfirmed); - } - - public Task GetNormalizedEmailAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).NormalizedEmail); - } - - public Task> GetClaimsAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult>(((MongoUser)user).Claims); - } - - public Task GetPhoneNumberAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).PhoneNumber); - } - - public Task GetPhoneNumberConfirmedAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).PhoneNumberConfirmed); - } - - public Task GetTwoFactorEnabledAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).TwoFactorEnabled); - } - - public Task GetLockoutEndDateAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).LockoutEnd); - } - - public Task GetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).AccessFailedCount); - } - - public Task GetLockoutEnabledAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).LockoutEnabled); - } - - public Task GetTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).GetToken(loginProvider, name)); - } - - public Task GetAuthenticatorKeyAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).GetToken(InternalLoginProvider, AuthenticatorKeyTokenName)); - } - - public Task HasPasswordAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(!string.IsNullOrWhiteSpace(((MongoUser)user).PasswordHash)); - } - - public Task CountCodesAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName)?.Split(';').Length ?? 0); - } - - public Task SetUserNameAsync(IdentityUser user, string userName, CancellationToken cancellationToken) - { - ((MongoUser)user).UserName = userName; - - return TaskHelper.Done; - } - - public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, CancellationToken cancellationToken) - { - ((MongoUser)user).NormalizedUserName = normalizedName; - - return TaskHelper.Done; - } - - public Task SetPasswordHashAsync(IdentityUser user, string passwordHash, CancellationToken cancellationToken) - { - ((MongoUser)user).PasswordHash = passwordHash; - - return TaskHelper.Done; - } - - public Task AddToRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) - { - ((MongoUser)user).AddRole(roleName); - - return TaskHelper.Done; - } - - public Task RemoveFromRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) - { - ((MongoUser)user).RemoveRole(roleName); - - return TaskHelper.Done; - } - - public Task AddLoginAsync(IdentityUser user, UserLoginInfo login, CancellationToken cancellationToken) - { - ((MongoUser)user).AddLogin(login); - - return TaskHelper.Done; - } - - public Task RemoveLoginAsync(IdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) - { - ((MongoUser)user).RemoveLogin(loginProvider, providerKey); - - return TaskHelper.Done; - } - - public Task SetSecurityStampAsync(IdentityUser user, string stamp, CancellationToken cancellationToken) - { - ((MongoUser)user).SecurityStamp = stamp; - - return TaskHelper.Done; - } - - public Task SetEmailAsync(IdentityUser user, string email, CancellationToken cancellationToken) - { - ((MongoUser)user).Email = email; - - return TaskHelper.Done; - } - - public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken) - { - ((MongoUser)user).EmailConfirmed = confirmed; - - return TaskHelper.Done; - } - - public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail, CancellationToken cancellationToken) - { - ((MongoUser)user).NormalizedEmail = normalizedEmail; - - return TaskHelper.Done; - } - - public Task AddClaimsAsync(IdentityUser user, IEnumerable claims, CancellationToken cancellationToken) - { - ((MongoUser)user).AddClaims(claims); - - return TaskHelper.Done; - } - - public Task ReplaceClaimAsync(IdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) - { - ((MongoUser)user).ReplaceClaim(claim, newClaim); - - return TaskHelper.Done; - } - - public Task RemoveClaimsAsync(IdentityUser user, IEnumerable claims, CancellationToken cancellationToken) - { - ((MongoUser)user).RemoveClaims(claims); - - return TaskHelper.Done; - } - - public Task SetPhoneNumberAsync(IdentityUser user, string phoneNumber, CancellationToken cancellationToken) - { - ((MongoUser)user).PhoneNumber = phoneNumber; - - return TaskHelper.Done; - } - - public Task SetPhoneNumberConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken) - { - ((MongoUser)user).PhoneNumberConfirmed = confirmed; - - return TaskHelper.Done; - } - - public Task SetTwoFactorEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken) - { - ((MongoUser)user).TwoFactorEnabled = enabled; - - return TaskHelper.Done; - } - - public Task SetLockoutEndDateAsync(IdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) - { - ((MongoUser)user).LockoutEnd = lockoutEnd?.UtcDateTime; - - return TaskHelper.Done; - } - - public Task IncrementAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) - { - ((MongoUser)user).AccessFailedCount++; - - return Task.FromResult(((MongoUser)user).AccessFailedCount); - } - - public Task ResetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) - { - ((MongoUser)user).AccessFailedCount = 0; - - return TaskHelper.Done; - } - - public Task SetLockoutEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken) - { - ((MongoUser)user).LockoutEnabled = enabled; - - return TaskHelper.Done; - } - - public Task SetTokenAsync(IdentityUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) - { - ((MongoUser)user).SetToken(loginProvider, name, value); - - return TaskHelper.Done; - } - - public Task RemoveTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) - { - ((MongoUser)user).RemoveToken(loginProvider, name); - - return TaskHelper.Done; - } - - public Task SetAuthenticatorKeyAsync(IdentityUser user, string key, CancellationToken cancellationToken) - { - ((MongoUser)user).SetToken(InternalLoginProvider, AuthenticatorKeyTokenName, key); - - return TaskHelper.Done; - } - - public Task ReplaceCodesAsync(IdentityUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken) - { - ((MongoUser)user).SetToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", recoveryCodes)); - - return TaskHelper.Done; - } - - public Task RedeemCodeAsync(IdentityUser user, string code, CancellationToken cancellationToken) - { - var mergedCodes = ((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName) ?? string.Empty; - - var splitCodes = mergedCodes.Split(';'); - if (splitCodes.Contains(code)) - { - var updatedCodes = new List(splitCodes.Where(s => s != code)); - - ((MongoUser)user).SetToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", updatedCodes)); - - return TaskHelper.True; - } - - return TaskHelper.False; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj deleted file mode 100644 index 88d90e5f7..000000000 --- a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/src/Squidex.Domain.Users/AssetUserPictureStore.cs b/src/Squidex.Domain.Users/AssetUserPictureStore.cs deleted file mode 100644 index 16219022b..000000000 --- a/src/Squidex.Domain.Users/AssetUserPictureStore.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.IO; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; - -namespace Squidex.Domain.Users -{ - public sealed class AssetUserPictureStore : IUserPictureStore - { - private readonly IAssetStore assetStore; - - public AssetUserPictureStore(IAssetStore assetStore) - { - Guard.NotNull(assetStore, nameof(assetStore)); - - this.assetStore = assetStore; - } - - public Task UploadAsync(string userId, Stream stream) - { - return assetStore.UploadAsync(userId, 0, "picture", stream, true); - } - - public async Task DownloadAsync(string userId) - { - var memoryStream = new MemoryStream(); - - await assetStore.DownloadAsync(userId, 0, "picture", memoryStream); - - memoryStream.Position = 0; - - return memoryStream; - } - } -} diff --git a/src/Squidex.Domain.Users/DefaultUserResolver.cs b/src/Squidex.Domain.Users/DefaultUserResolver.cs deleted file mode 100644 index 1112ee86b..000000000 --- a/src/Squidex.Domain.Users/DefaultUserResolver.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Squidex.Infrastructure; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Users -{ - public sealed class DefaultUserResolver : IUserResolver - { - private readonly UserManager userManager; - private readonly IUserFactory userFactory; - - public DefaultUserResolver(UserManager userManager, IUserFactory userFactory) - { - Guard.NotNull(userManager, nameof(userManager)); - Guard.NotNull(userFactory, nameof(userFactory)); - - this.userManager = userManager; - this.userFactory = userFactory; - } - - public async Task CreateUserIfNotExists(string email, bool invited) - { - var user = userFactory.Create(email); - - try - { - var result = await userManager.CreateAsync(user); - - if (result.Succeeded) - { - var values = new UserValues { DisplayName = email, Invited = invited }; - - await userManager.UpdateAsync(user, values); - } - - return result.Succeeded; - } - catch - { - return false; - } - } - - public async Task FindByIdOrEmailAsync(string idOrEmail) - { - if (userFactory.IsId(idOrEmail)) - { - return await userManager.FindByIdWithClaimsAsync(idOrEmail); - } - else - { - return await userManager.FindByEmailWithClaimsAsyncAsync(idOrEmail); - } - } - - public async Task> QueryByEmailAsync(string email) - { - var result = await userManager.QueryByEmailAsync(email); - - return result.OfType().ToList(); - } - - public async Task> QueryManyAsync(string[] ids) - { - var result = await userManager.QueryByIdsAync(ids); - - return result.OfType().ToDictionary(x => x.Id); - } - } -} diff --git a/src/Squidex.Domain.Users/DefaultXmlRepository.cs b/src/Squidex.Domain.Users/DefaultXmlRepository.cs deleted file mode 100644 index 95ee76023..000000000 --- a/src/Squidex.Domain.Users/DefaultXmlRepository.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Xml.Linq; -using Microsoft.AspNetCore.DataProtection.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Users -{ - public sealed class DefaultXmlRepository : IXmlRepository - { - private readonly ISnapshotStore store; - - [CollectionName("XmlRepository")] - public sealed class State - { - public string Xml { get; set; } - } - - public DefaultXmlRepository(ISnapshotStore store) - { - Guard.NotNull(store, nameof(store)); - - this.store = store; - } - - public IReadOnlyCollection GetAllElements() - { - var result = new List(); - - store.ReadAllAsync((state, version) => - { - result.Add(XElement.Parse(state.Xml)); - - return TaskHelper.Done; - }).Wait(); - - return result; - } - - public void StoreElement(XElement element, string friendlyName) - { - store.WriteAsync(friendlyName, new State { Xml = element.ToString() }, EtagVersion.Any, EtagVersion.Any).Wait(); - } - } -} diff --git a/src/Squidex.Domain.Users/PwnedPasswordValidator.cs b/src/Squidex.Domain.Users/PwnedPasswordValidator.cs deleted file mode 100644 index 08a2f8320..000000000 --- a/src/Squidex.Domain.Users/PwnedPasswordValidator.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using SharpPwned.NET; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Domain.Users -{ - public sealed class PwnedPasswordValidator : IPasswordValidator - { - private const string ErrorCode = "PwnedError"; - private const string ErrorText = "This password has previously appeared in a data breach and should never be used. If you've ever used it anywhere before, change it!"; - private static readonly IdentityResult Error = IdentityResult.Failed(new IdentityError { Code = ErrorCode, Description = ErrorText }); - - private readonly HaveIBeenPwnedRestClient client = new HaveIBeenPwnedRestClient(); - private readonly ISemanticLog log; - - public PwnedPasswordValidator(ISemanticLog log) - { - Guard.NotNull(log, nameof(log)); - - this.log = log; - } - - public async Task ValidateAsync(UserManager manager, IdentityUser user, string password) - { - try - { - var isBreached = await client.IsPasswordPwned(password); - - if (isBreached) - { - return Error; - } - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("operation", "CheckPasswordPwned") - .WriteProperty("status", "Failed")); - } - - return IdentityResult.Success; - } - } -} diff --git a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj deleted file mode 100644 index 135fee181..000000000 --- a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/src/Squidex.Domain.Users/UserManagerExtensions.cs b/src/Squidex.Domain.Users/UserManagerExtensions.cs deleted file mode 100644 index 880663a92..000000000 --- a/src/Squidex.Domain.Users/UserManagerExtensions.cs +++ /dev/null @@ -1,286 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Squidex.Shared.Identity; - -namespace Squidex.Domain.Users -{ - public static class UserManagerExtensions - { - public static async Task GetUserWithClaimsAsync(this UserManager userManager, ClaimsPrincipal principal) - { - if (principal == null) - { - return null; - } - - var user = await userManager.FindByIdWithClaimsAsync(userManager.GetUserId(principal)); - - return user; - } - - public static async Task ResolveUserAsync(this UserManager userManager, IdentityUser user) - { - if (user == null) - { - return null; - } - - var claims = await userManager.GetClaimsAsync(user); - - return new UserWithClaims(user, claims); - } - - public static async Task FindByIdWithClaimsAsync(this UserManager userManager, string id) - { - if (id == null) - { - return null; - } - - var user = await userManager.FindByIdAsync(id); - - return await userManager.ResolveUserAsync(user); - } - - public static async Task FindByEmailWithClaimsAsyncAsync(this UserManager userManager, string email) - { - if (email == null) - { - return null; - } - - var user = await userManager.FindByEmailAsync(email); - - return await userManager.ResolveUserAsync(user); - } - - public static async Task FindByLoginWithClaimsAsync(this UserManager userManager, string loginProvider, string providerKey) - { - if (loginProvider == null || providerKey == null) - { - return null; - } - - var user = await userManager.FindByLoginAsync(loginProvider, providerKey); - - return await userManager.ResolveUserAsync(user); - } - - public static Task CountByEmailAsync(this UserManager userManager, string email = null) - { - var count = QueryUsers(userManager, email).LongCount(); - - return Task.FromResult(count); - } - - public static async Task> QueryByIdsAync(this UserManager userManager, string[] ids) - { - var users = userManager.Users.Where(x => ids.Contains(x.Id)).ToList(); - - var result = await userManager.ResolveUsersAsync(users); - - return result.ToList(); - } - - public static async Task> QueryByEmailAsync(this UserManager userManager, string email = null, int take = 10, int skip = 0) - { - var users = QueryUsers(userManager, email).Skip(skip).Take(take).ToList(); - - var result = await userManager.ResolveUsersAsync(users); - - return result.ToList(); - } - - public static Task ResolveUsersAsync(this UserManager userManager, IEnumerable users) - { - return Task.WhenAll(users.Select(async user => - { - return await userManager.ResolveUserAsync(user); - })); - } - - public static IQueryable QueryUsers(UserManager userManager, string email = null) - { - var result = userManager.Users; - - if (!string.IsNullOrWhiteSpace(email)) - { - var normalizedEmail = userManager.NormalizeKey(email); - - result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail)); - } - - return result; - } - - public static async Task CreateAsync(this UserManager userManager, IUserFactory factory, UserValues values) - { - var user = factory.Create(values.Email); - - try - { - await DoChecked(() => userManager.CreateAsync(user), "Cannot create user."); - - var claims = values.ToClaims(true); - - if (claims.Count > 0) - { - await DoChecked(() => userManager.AddClaimsAsync(user, claims), "Cannot add user."); - } - - if (!string.IsNullOrWhiteSpace(values.Password)) - { - await DoChecked(() => userManager.AddPasswordAsync(user, values.Password), "Cannot create user."); - } - } - catch - { - await userManager.DeleteAsync(user); - - throw; - } - - return await userManager.ResolveUserAsync(user); - } - - public static async Task UpdateAsync(this UserManager userManager, string id, UserValues values) - { - var user = await userManager.FindByIdAsync(id); - - if (user == null) - { - throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); - } - - await UpdateAsync(userManager, user, values); - - return await userManager.ResolveUserAsync(user); - } - - public static Task GenerateClientSecretAsync(this UserManager userManager, IdentityUser user) - { - var claims = new List { new Claim(SquidexClaimTypes.ClientSecret, RandomHash.New()) }; - - return userManager.SyncClaimsAsync(user, claims); - } - - public static async Task UpdateSafeAsync(this UserManager userManager, IdentityUser user, UserValues values) - { - try - { - await userManager.UpdateAsync(user, values); - - return IdentityResult.Success; - } - catch (ValidationException ex) - { - return IdentityResult.Failed(ex.Errors.Select(x => new IdentityError { Description = x.Message }).ToArray()); - } - } - - public static async Task UpdateAsync(this UserManager userManager, IdentityUser user, UserValues values) - { - if (user == null) - { - throw new DomainObjectNotFoundException("Id", typeof(IdentityUser)); - } - - if (!string.IsNullOrWhiteSpace(values.Email) && values.Email != user.Email) - { - await DoChecked(() => userManager.SetEmailAsync(user, values.Email), "Cannot update email."); - await DoChecked(() => userManager.SetUserNameAsync(user, values.Email), "Cannot update email."); - } - - await DoChecked(() => userManager.SyncClaimsAsync(user, values.ToClaims(false)), "Cannot update user."); - - if (!string.IsNullOrWhiteSpace(values.Password)) - { - await DoChecked(() => userManager.RemovePasswordAsync(user), "Cannot replace password."); - await DoChecked(() => userManager.AddPasswordAsync(user, values.Password), "Cannot replace password."); - } - } - - public static async Task LockAsync(this UserManager userManager, string id) - { - var user = await userManager.FindByIdAsync(id); - - if (user == null) - { - throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); - } - - await DoChecked(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100)), "Cannot lock user."); - - return await userManager.ResolveUserAsync(user); - } - - public static async Task UnlockAsync(this UserManager userManager, string id) - { - var user = await userManager.FindByIdAsync(id); - - if (user == null) - { - throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); - } - - await DoChecked(() => userManager.SetLockoutEndDateAsync(user, null), "Cannot unlock user."); - - return await userManager.ResolveUserAsync(user); - } - - private static async Task DoChecked(Func> action, string message) - { - var result = await action(); - - if (!result.Succeeded) - { - throw new ValidationException(message, result.Errors.Select(x => new ValidationError(x.Description)).ToArray()); - } - } - - public static async Task SyncClaimsAsync(this UserManager userManager, IdentityUser user, List claims) - { - if (claims.Any()) - { - var oldClaims = await userManager.GetClaimsAsync(user); - - var oldClaimsToRemove = new List(); - - foreach (var oldClaim in oldClaims) - { - if (claims.Any(x => x.Type == oldClaim.Type)) - { - oldClaimsToRemove.Add(oldClaim); - } - } - - if (oldClaimsToRemove.Count > 0) - { - var result = await userManager.RemoveClaimsAsync(user, oldClaimsToRemove); - - if (!result.Succeeded) - { - return result; - } - } - - return await userManager.AddClaimsAsync(user, claims.Where(x => !string.IsNullOrWhiteSpace(x.Value))); - } - - return IdentityResult.Success; - } - } -} diff --git a/src/Squidex.Domain.Users/UserWithClaims.cs b/src/Squidex.Domain.Users/UserWithClaims.cs deleted file mode 100644 index cc42fedce..000000000 --- a/src/Squidex.Domain.Users/UserWithClaims.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using Microsoft.AspNetCore.Identity; -using Squidex.Infrastructure; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Users -{ - public sealed class UserWithClaims : IUser - { - public IdentityUser Identity { get; } - - public List Claims { get; } - - public string Id - { - get { return Identity.Id; } - } - - public string Email - { - get { return Identity.Email; } - } - - public bool IsLocked - { - get { return Identity.LockoutEnd > DateTime.Now.ToUniversalTime(); } - } - - IReadOnlyList IUser.Claims - { - get { return Claims; } - } - - public UserWithClaims(IdentityUser user, IEnumerable claims) - { - Guard.NotNull(user, nameof(user)); - Guard.NotNull(claims, nameof(claims)); - - Identity = user; - - Claims = claims.ToList(); - } - } -} diff --git a/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs b/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs deleted file mode 100644 index c50b554a1..000000000 --- a/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs +++ /dev/null @@ -1,142 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; - -namespace Squidex.Infrastructure.Assets -{ - public class AzureBlobAssetStore : IAssetStore, IInitializable - { - private readonly string containerName; - private readonly string connectionString; - private CloudBlobContainer blobContainer; - - public AzureBlobAssetStore(string connectionString, string containerName) - { - Guard.NotNullOrEmpty(containerName, nameof(containerName)); - Guard.NotNullOrEmpty(connectionString, nameof(connectionString)); - - this.connectionString = connectionString; - this.containerName = containerName; - } - - public async Task InitializeAsync(CancellationToken ct = default) - { - try - { - var storageAccount = CloudStorageAccount.Parse(connectionString); - - var blobClient = storageAccount.CreateCloudBlobClient(); - var blobReference = blobClient.GetContainerReference(containerName); - - await blobReference.CreateIfNotExistsAsync(); - - blobContainer = blobReference; - } - catch (Exception ex) - { - throw new ConfigurationException($"Cannot connect to blob container '{containerName}'.", ex); - } - } - - public string GeneratePublicUrl(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - if (blobContainer.Properties.PublicAccess != BlobContainerPublicAccessType.Blob) - { - var blob = blobContainer.GetBlockBlobReference(fileName); - - return blob.Uri.ToString(); - } - - return null; - } - - public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName)); - Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); - - try - { - var sourceBlob = blobContainer.GetBlockBlobReference(sourceFileName); - - var targetBlob = blobContainer.GetBlobReference(targetFileName); - - await targetBlob.StartCopyAsync(sourceBlob.Uri, null, AccessCondition.GenerateIfNotExistsCondition(), null, null, ct); - - while (targetBlob.CopyState.Status == CopyStatus.Pending) - { - ct.ThrowIfCancellationRequested(); - - await Task.Delay(50, ct); - await targetBlob.FetchAttributesAsync(null, null, null, ct); - } - - if (targetBlob.CopyState.Status != CopyStatus.Success) - { - throw new StorageException($"Copy of temporary file failed: {targetBlob.CopyState.Status}"); - } - } - catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 409) - { - throw new AssetAlreadyExistsException(targetFileName); - } - catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404) - { - throw new AssetNotFoundException(sourceFileName, ex); - } - } - - public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - try - { - var blob = blobContainer.GetBlockBlobReference(fileName); - - await blob.DownloadToStreamAsync(stream, null, null, null, ct); - } - catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404) - { - throw new AssetNotFoundException(fileName, ex); - } - } - - public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - try - { - var tempBlob = blobContainer.GetBlockBlobReference(fileName); - - await tempBlob.UploadFromStreamAsync(stream, overwrite ? null : AccessCondition.GenerateIfNotExistsCondition(), null, null, ct); - } - catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 409) - { - throw new AssetAlreadyExistsException(fileName); - } - } - - public Task DeleteAsync(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - var blob = blobContainer.GetBlockBlobReference(fileName); - - return blob.DeleteIfExistsAsync(); - } - } -} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs deleted file mode 100644 index 27431204a..000000000 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs +++ /dev/null @@ -1,138 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.ObjectModel; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.Documents; -using Microsoft.Azure.Documents.Client; -using Newtonsoft.Json; - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed partial class CosmosDbEventStore : DisposableObjectBase, IEventStore, IInitializable - { - private readonly DocumentClient documentClient; - private readonly Uri collectionUri; - private readonly Uri databaseUri; - private readonly string masterKey; - private readonly string databaseId; - private readonly JsonSerializerSettings serializerSettings; - - public JsonSerializerSettings SerializerSettings - { - get { return serializerSettings; } - } - - public string DatabaseId - { - get { return databaseId; } - } - - public string MasterKey - { - get { return masterKey; } - } - - public Uri ServiceUri - { - get { return documentClient.ServiceEndpoint; } - } - - public CosmosDbEventStore(DocumentClient documentClient, string masterKey, string database, JsonSerializerSettings serializerSettings) - { - Guard.NotNull(documentClient, nameof(documentClient)); - Guard.NotNull(serializerSettings, nameof(serializerSettings)); - Guard.NotNullOrEmpty(masterKey, nameof(masterKey)); - Guard.NotNullOrEmpty(database, nameof(database)); - - this.documentClient = documentClient; - - databaseUri = UriFactory.CreateDatabaseUri(database); - databaseId = database; - - collectionUri = UriFactory.CreateDocumentCollectionUri(database, Constants.Collection); - - this.masterKey = masterKey; - - this.serializerSettings = serializerSettings; - } - - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - documentClient.Dispose(); - } - } - - public async Task InitializeAsync(CancellationToken ct = default) - { - await documentClient.CreateDatabaseIfNotExistsAsync(new Database { Id = databaseId }); - - await documentClient.CreateDocumentCollectionIfNotExistsAsync(databaseUri, - new DocumentCollection - { - PartitionKey = new PartitionKeyDefinition - { - Paths = new Collection - { - "/PartitionId" - } - }, - Id = Constants.LeaseCollection - }); - - await documentClient.CreateDocumentCollectionIfNotExistsAsync(databaseUri, - new DocumentCollection - { - PartitionKey = new PartitionKeyDefinition - { - Paths = new Collection - { - "/eventStream" - } - }, - IndexingPolicy = new IndexingPolicy - { - IncludedPaths = new Collection - { - new IncludedPath - { - Path = "/*", - Indexes = new Collection - { - Index.Range(DataType.Number), - Index.Range(DataType.String) - } - } - } - }, - UniqueKeyPolicy = new UniqueKeyPolicy - { - UniqueKeys = new Collection - { - new UniqueKey - { - Paths = new Collection - { - "/eventStream", - "/eventStreamOffset" - } - } - } - }, - Id = Constants.Collection - }, - new RequestOptions - { - PartitionKey = new PartitionKey("/eventStream") - }); - } - } -} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs deleted file mode 100644 index ada171f2a..000000000 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs +++ /dev/null @@ -1,142 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.Documents; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.EventSourcing -{ - public delegate bool EventPredicate(EventData data); - - public partial class CosmosDbEventStore : IEventStore, IInitializable - { - public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null) - { - Guard.NotNull(subscriber, nameof(subscriber)); - - ThrowIfDisposed(); - - return new CosmosDbSubscription(this, subscriber, streamFilter, position); - } - - public Task CreateIndexAsync(string property) - { - Guard.NotNullOrEmpty(property, nameof(property)); - - ThrowIfDisposed(); - - return TaskHelper.Done; - } - - public async Task> QueryAsync(string streamName, long streamPosition = 0) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - - ThrowIfDisposed(); - - using (Profiler.TraceMethod()) - { - var query = FilterBuilder.ByStreamName(streamName, streamPosition - MaxCommitSize); - - var result = new List(); - - await documentClient.QueryAsync(collectionUri, query, commit => - { - var eventStreamOffset = (int)commit.EventStreamOffset; - - var commitTimestamp = commit.Timestamp; - var commitOffset = 0; - - foreach (var @event in commit.Events) - { - eventStreamOffset++; - - if (eventStreamOffset >= streamPosition) - { - var eventData = @event.ToEventData(); - var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); - - result.Add(new StoredEvent(streamName, eventToken, eventStreamOffset, eventData)); - } - } - - return TaskHelper.Done; - }); - - return result; - } - } - - public Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default) - { - Guard.NotNull(callback, nameof(callback)); - Guard.NotNullOrEmpty(property, nameof(property)); - Guard.NotNull(value, nameof(value)); - - ThrowIfDisposed(); - - StreamPosition lastPosition = position; - - var filterDefinition = FilterBuilder.CreateByProperty(property, value, lastPosition); - var filterExpression = FilterBuilder.CreateExpression(property, value); - - return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); - } - - public Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default) - { - Guard.NotNull(callback, nameof(callback)); - - ThrowIfDisposed(); - - StreamPosition lastPosition = position; - - var filterDefinition = FilterBuilder.CreateByFilter(streamFilter, lastPosition); - var filterExpression = FilterBuilder.CreateExpression(null, null); - - return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); - } - - private async Task QueryAsync(Func callback, StreamPosition lastPosition, SqlQuerySpec query, EventPredicate filterExpression, CancellationToken ct = default) - { - using (Profiler.TraceMethod()) - { - await documentClient.QueryAsync(collectionUri, query, async commit => - { - var eventStreamOffset = (int)commit.EventStreamOffset; - - var commitTimestamp = commit.Timestamp; - var commitOffset = 0; - - foreach (var @event in commit.Events) - { - eventStreamOffset++; - - if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) - { - var eventData = @event.ToEventData(); - - if (filterExpression(eventData)) - { - var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); - - await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData)); - } - } - - commitOffset++; - } - }, ct); - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs deleted file mode 100644 index 0c9186e15..000000000 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs +++ /dev/null @@ -1,149 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using Microsoft.Azure.Documents; -using Microsoft.Azure.Documents.Client; -using NodaTime; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.EventSourcing -{ - public partial class CosmosDbEventStore - { - private const int MaxWriteAttempts = 20; - private const int MaxCommitSize = 10; - - public Task DeleteStreamAsync(string streamName) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - - ThrowIfDisposed(); - - var query = FilterBuilder.AllIds(streamName); - - return documentClient.QueryAsync(collectionUri, query, commit => - { - var documentUri = UriFactory.CreateDocumentUri(databaseId, Constants.Collection, commit.Id.ToString()); - - return documentClient.DeleteDocumentAsync(documentUri); - }); - } - - public Task AppendAsync(Guid commitId, string streamName, ICollection events) - { - return AppendAsync(commitId, streamName, EtagVersion.Any, events); - } - - public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) - { - Guard.NotEmpty(commitId, nameof(commitId)); - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNull(events, nameof(events)); - Guard.LessThan(events.Count, MaxCommitSize, "events.Count"); - - ThrowIfDisposed(); - - using (Profiler.TraceMethod()) - { - if (events.Count == 0) - { - return; - } - - var currentVersion = await GetEventStreamOffsetAsync(streamName); - - if (expectedVersion > EtagVersion.Any && expectedVersion != currentVersion) - { - throw new WrongEventVersionException(currentVersion, expectedVersion); - } - - var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events); - - for (var attempt = 0; attempt < MaxWriteAttempts; attempt++) - { - try - { - await documentClient.CreateDocumentAsync(collectionUri, commit); - - return; - } - catch (DocumentClientException ex) - { - if (ex.StatusCode == HttpStatusCode.Conflict) - { - currentVersion = await GetEventStreamOffsetAsync(streamName); - - if (expectedVersion > EtagVersion.Any) - { - throw new WrongEventVersionException(currentVersion, expectedVersion); - } - - if (attempt < MaxWriteAttempts) - { - expectedVersion = currentVersion; - } - else - { - throw new TimeoutException("Could not acquire a free slot for the commit within the provided time."); - } - } - else - { - throw; - } - } - } - } - } - - private async Task GetEventStreamOffsetAsync(string streamName) - { - var query = - documentClient.CreateDocumentQuery(collectionUri, - FilterBuilder.LastPosition(streamName)); - - var document = await query.FirstOrDefaultAsync(); - - if (document != null) - { - return document.EventStreamOffset + document.EventsCount; - } - - return EtagVersion.Empty; - } - - private static CosmosDbEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection events) - { - var commitEvents = new CosmosDbEvent[events.Count]; - - var i = 0; - - foreach (var e in events) - { - var mongoEvent = CosmosDbEvent.FromEventData(e); - - commitEvents[i++] = mongoEvent; - } - - var mongoCommit = new CosmosDbEventCommit - { - Id = commitId, - Events = commitEvents, - EventsCount = events.Count, - EventStream = streamName, - EventStreamOffset = expectedVersion, - Timestamp = SystemClock.Instance.GetCurrentInstant().ToUnixTimeTicks() - }; - - return mongoCommit; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs deleted file mode 100644 index d2bb4b9b2..000000000 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs +++ /dev/null @@ -1,151 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.Documents; -using Microsoft.Azure.Documents.ChangeFeedProcessor.FeedProcessing; -using Newtonsoft.Json; -using Squidex.Infrastructure.Tasks; -using Builder = Microsoft.Azure.Documents.ChangeFeedProcessor.ChangeFeedProcessorBuilder; -using Collection = Microsoft.Azure.Documents.ChangeFeedProcessor.DocumentCollectionInfo; -using Options = Microsoft.Azure.Documents.ChangeFeedProcessor.ChangeFeedProcessorOptions; - -#pragma warning disable IDE0017 // Simplify object initialization - -namespace Squidex.Infrastructure.EventSourcing -{ - internal sealed class CosmosDbSubscription : IEventSubscription, IChangeFeedObserverFactory, IChangeFeedObserver - { - private readonly TaskCompletionSource processorStopRequested = new TaskCompletionSource(); - private readonly Task processorTask; - private readonly CosmosDbEventStore store; - private readonly Regex regex; - private readonly string hostName; - private readonly IEventSubscriber subscriber; - - public CosmosDbSubscription(CosmosDbEventStore store, IEventSubscriber subscriber, string streamFilter, string position = null) - { - this.store = store; - - var fromBeginning = string.IsNullOrWhiteSpace(position); - - if (fromBeginning) - { - hostName = $"squidex.{DateTime.UtcNow.Ticks.ToString()}"; - } - else - { - hostName = position; - } - - if (!StreamFilter.IsAll(streamFilter)) - { - regex = new Regex(streamFilter); - } - - this.subscriber = subscriber; - - processorTask = Task.Run(async () => - { - try - { - Collection CreateCollection(string name) - { - var collection = new Collection(); - - collection.CollectionName = name; - collection.DatabaseName = store.DatabaseId; - collection.MasterKey = store.MasterKey; - collection.Uri = store.ServiceUri; - - return collection; - } - - var processor = - await new Builder() - .WithFeedCollection(CreateCollection(Constants.Collection)) - .WithLeaseCollection(CreateCollection(Constants.LeaseCollection)) - .WithHostName(hostName) - .WithProcessorOptions(new Options { StartFromBeginning = fromBeginning, LeasePrefix = hostName }) - .WithObserverFactory(this) - .BuildAsync(); - - await processor.StartAsync(); - await processorStopRequested.Task; - await processor.StopAsync(); - } - catch (Exception ex) - { - await subscriber.OnErrorAsync(this, ex); - } - }); - } - - public IChangeFeedObserver CreateObserver() - { - return this; - } - - public async Task CloseAsync(IChangeFeedObserverContext context, ChangeFeedObserverCloseReason reason) - { - if (reason == ChangeFeedObserverCloseReason.ObserverError) - { - await subscriber.OnErrorAsync(this, new InvalidOperationException("Change feed observer failed.")); - } - } - - public Task OpenAsync(IChangeFeedObserverContext context) - { - return TaskHelper.Done; - } - - public async Task ProcessChangesAsync(IChangeFeedObserverContext context, IReadOnlyList docs, CancellationToken cancellationToken) - { - if (!processorStopRequested.Task.IsCompleted) - { - foreach (var document in docs) - { - if (!processorStopRequested.Task.IsCompleted) - { - var streamName = document.GetPropertyValue("eventStream"); - - if (regex == null || regex.IsMatch(streamName)) - { - var commit = JsonConvert.DeserializeObject(document.ToString(), store.SerializerSettings); - - var eventStreamOffset = (int)commit.EventStreamOffset; - - foreach (var @event in commit.Events) - { - eventStreamOffset++; - - var eventData = @event.ToEventData(); - - await subscriber.OnEventAsync(this, new StoredEvent(commit.EventStream, hostName, eventStreamOffset, eventData)); - } - } - } - } - } - } - - public void WakeUp() - { - } - - public Task StopAsync() - { - processorStopRequested.SetResult(true); - - return processorTask; - } - } -} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs deleted file mode 100644 index 419248c2f..000000000 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs +++ /dev/null @@ -1,156 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Microsoft.Azure.Documents; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Infrastructure.EventSourcing -{ - internal static class FilterBuilder - { - public static SqlQuerySpec AllIds(string streamName) - { - var query = - $"SELECT TOP 1 " + - $" e.id," + - $" e.eventsCount " + - $"FROM {Constants.Collection} e " + - $"WHERE " + - $" e.eventStream = @name " + - $"ORDER BY e.eventStreamOffset DESC"; - - var parameters = new SqlParameterCollection - { - new SqlParameter("@name", streamName) - }; - - return new SqlQuerySpec(query, parameters); - } - - public static SqlQuerySpec LastPosition(string streamName) - { - var query = - $"SELECT TOP 1 " + - $" e.eventStreamOffset," + - $" e.eventsCount " + - $"FROM {Constants.Collection} e " + - $"WHERE " + - $" e.eventStream = @name " + - $"ORDER BY e.eventStreamOffset DESC"; - - var parameters = new SqlParameterCollection - { - new SqlParameter("@name", streamName) - }; - - return new SqlQuerySpec(query, parameters); - } - - public static SqlQuerySpec ByStreamName(string streamName, long streamPosition = 0) - { - var query = - $"SELECT * " + - $"FROM {Constants.Collection} e " + - $"WHERE " + - $" e.eventStream = @name " + - $"AND e.eventStreamOffset >= @position " + - $"ORDER BY e.eventStreamOffset ASC"; - - var parameters = new SqlParameterCollection - { - new SqlParameter("@name", streamName), - new SqlParameter("@position", streamPosition) - }; - - return new SqlQuerySpec(query, parameters); - } - - public static SqlQuerySpec CreateByProperty(string property, object value, StreamPosition streamPosition) - { - var filters = new List(); - - var parameters = new SqlParameterCollection(); - - filters.ForPosition(parameters, streamPosition); - filters.ForProperty(parameters, property, value); - - return BuildQuery(filters, parameters); - } - - public static SqlQuerySpec CreateByFilter(string streamFilter, StreamPosition streamPosition) - { - var filters = new List(); - - var parameters = new SqlParameterCollection(); - - filters.ForPosition(parameters, streamPosition); - filters.ForRegex(parameters, streamFilter); - - return BuildQuery(filters, parameters); - } - - private static SqlQuerySpec BuildQuery(IEnumerable filters, SqlParameterCollection parameters) - { - var query = $"SELECT * FROM {Constants.Collection} e WHERE {string.Join(" AND ", filters)} ORDER BY e.timestamp"; - - return new SqlQuerySpec(query, parameters); - } - - private static void ForProperty(this ICollection filters, SqlParameterCollection parameters, string property, object value) - { - filters.Add($"ARRAY_CONTAINS(e.events, {{ \"header\": {{ \"{property}\": @value }} }}, true)"); - - parameters.Add(new SqlParameter("@value", value)); - } - - private static void ForRegex(this ICollection filters, SqlParameterCollection parameters, string streamFilter) - { - if (!StreamFilter.IsAll(streamFilter)) - { - if (streamFilter.Contains("^")) - { - filters.Add($"STARTSWITH(e.eventStream, @filter)"); - } - else - { - filters.Add($"e.eventStream = @filter"); - } - - parameters.Add(new SqlParameter("@filter", streamFilter)); - } - } - - private static void ForPosition(this ICollection filters, SqlParameterCollection parameters, StreamPosition streamPosition) - { - if (streamPosition.IsEndOfCommit) - { - filters.Add($"e.timestamp > @time"); - } - else - { - filters.Add($"e.timestamp >= @time"); - } - - parameters.Add(new SqlParameter("@time", streamPosition.Timestamp)); - } - - public static EventPredicate CreateExpression(string property, object value) - { - if (!string.IsNullOrWhiteSpace(property)) - { - var jsonValue = JsonValue.Create(value); - - return x => x.Headers.TryGetValue(property, out var p) && p.Equals(jsonValue); - } - else - { - return x => true; - } - } - } -} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs deleted file mode 100644 index c24e93ff1..000000000 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.Documents; -using Microsoft.Azure.Documents.Client; -using Microsoft.Azure.Documents.Linq; - -namespace Squidex.Infrastructure.EventSourcing -{ - internal static class FilterExtensions - { - public static async Task FirstOrDefaultAsync(this IQueryable queryable, CancellationToken ct = default) - { - var documentQuery = queryable.AsDocumentQuery(); - - using (documentQuery) - { - if (documentQuery.HasMoreResults) - { - var results = await documentQuery.ExecuteNextAsync(ct); - - return results.FirstOrDefault(); - } - } - - return default; - } - - public static Task QueryAsync(this DocumentClient documentClient, Uri collectionUri, SqlQuerySpec querySpec, Func handler, CancellationToken ct = default) - { - var query = documentClient.CreateDocumentQuery(collectionUri, querySpec); - - return query.QueryAsync(handler, ct); - } - - public static async Task QueryAsync(this IQueryable queryable, Func handler, CancellationToken ct = default) - { - var documentQuery = queryable.AsDocumentQuery(); - - using (documentQuery) - { - while (documentQuery.HasMoreResults && !ct.IsCancellationRequested) - { - var items = await documentQuery.ExecuteNextAsync(ct); - - foreach (var item in items) - { - await handler(item); - } - } - } - } - } -} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs deleted file mode 100644 index f0626ee5d..000000000 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.EventSourcing -{ - internal sealed class StreamPosition - { - public long Timestamp { get; } - - public long CommitOffset { get; } - - public long CommitSize { get; } - - public bool IsEndOfCommit - { - get { return CommitOffset == CommitSize - 1; } - } - - public StreamPosition(long timestamp, long commitOffset, long commitSize) - { - Timestamp = timestamp; - - CommitOffset = commitOffset; - CommitSize = commitSize; - } - - public static implicit operator string(StreamPosition position) - { - var parts = new object[] - { - position.Timestamp, - position.CommitOffset, - position.CommitSize - }; - - return string.Join("-", parts); - } - - public static implicit operator StreamPosition(string position) - { - if (!string.IsNullOrWhiteSpace(position)) - { - var parts = position.Split('-'); - - return new StreamPosition(long.Parse(parts[0]), long.Parse(parts[1]), long.Parse(parts[2])); - } - - return new StreamPosition(0, -1, -1); - } - } -} diff --git a/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj b/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj deleted file mode 100644 index 3811eab8a..000000000 --- a/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - netstandard2.0 - Squidex.Infrastructure - 7.3 - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs b/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs deleted file mode 100644 index 362972559..000000000 --- a/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using EventStore.ClientAPI; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace Squidex.Infrastructure.Diagnostics -{ - public sealed class GetEventStoreHealthCheck : IHealthCheck - { - private readonly IEventStoreConnection connection; - - public GetEventStoreHealthCheck(IEventStoreConnection connection) - { - Guard.NotNull(connection, nameof(connection)); - - this.connection = connection; - } - - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - await connection.ReadEventAsync("test", 1, false); - - return HealthCheckResult.Healthy("Application must query data from EventStore."); - } - } -} diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs deleted file mode 100644 index cc05285d8..000000000 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs +++ /dev/null @@ -1,78 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using EventStore.ClientAPI; -using Squidex.Infrastructure.Json; -using EventStoreData = EventStore.ClientAPI.EventData; - -namespace Squidex.Infrastructure.EventSourcing -{ - public static class Formatter - { - private static readonly HashSet PrivateHeaders = new HashSet { "$v", "$p", "$c", "$causedBy" }; - - public static StoredEvent Read(ResolvedEvent resolvedEvent, string prefix, IJsonSerializer serializer) - { - var @event = resolvedEvent.Event; - - var eventPayload = Encoding.UTF8.GetString(@event.Data); - var eventHeaders = GetHeaders(serializer, @event); - - var eventData = new EventData(@event.EventType, eventHeaders, eventPayload); - - var streamName = GetStreamName(prefix, @event); - - return new StoredEvent( - streamName, - resolvedEvent.OriginalEventNumber.ToString(), - resolvedEvent.Event.EventNumber, - eventData); - } - - private static string GetStreamName(string prefix, RecordedEvent @event) - { - var streamName = @event.EventStreamId; - - if (streamName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - streamName = streamName.Substring(prefix.Length + 1); - } - - return streamName; - } - - private static EnvelopeHeaders GetHeaders(IJsonSerializer serializer, RecordedEvent @event) - { - var headersJson = Encoding.UTF8.GetString(@event.Metadata); - var headers = serializer.Deserialize(headersJson); - - foreach (var key in headers.Keys.ToList()) - { - if (PrivateHeaders.Contains(key)) - { - headers.Remove(key); - } - } - - return headers; - } - - public static EventStoreData Write(EventData eventData, IJsonSerializer serializer) - { - var payload = Encoding.UTF8.GetBytes(eventData.Payload); - - var headersJson = serializer.Serialize(eventData.Headers); - var headersBytes = Encoding.UTF8.GetBytes(headersJson); - - return new EventStoreData(Guid.NewGuid(), eventData.Type, true, payload, headersBytes); - } - } -} diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs deleted file mode 100644 index d06c1ea15..000000000 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs +++ /dev/null @@ -1,224 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EventStore.ClientAPI; -using EventStore.ClientAPI.Exceptions; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class GetEventStore : IEventStore, IInitializable - { - private const int WritePageSize = 500; - private const int ReadPageSize = 500; - private readonly IEventStoreConnection connection; - private readonly IJsonSerializer serializer; - private readonly string prefix; - private readonly ProjectionClient projectionClient; - - public GetEventStore(IEventStoreConnection connection, IJsonSerializer serializer, string prefix, string projectionHost) - { - Guard.NotNull(connection, nameof(connection)); - Guard.NotNull(serializer, nameof(serializer)); - - this.connection = connection; - this.serializer = serializer; - - this.prefix = prefix?.Trim(' ', '-').WithFallback("squidex"); - - projectionClient = new ProjectionClient(connection, prefix, projectionHost); - } - - public async Task InitializeAsync(CancellationToken ct = default) - { - try - { - await connection.ConnectAsync(); - } - catch (Exception ex) - { - throw new ConfigurationException("Cannot connect to event store.", ex); - } - - await projectionClient.ConnectAsync(ct); - } - - public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null) - { - Guard.NotNull(streamFilter, nameof(streamFilter)); - - return new GetEventStoreSubscription(connection, subscriber, serializer, projectionClient, position, prefix, streamFilter); - } - - public Task CreateIndexAsync(string property) - { - Guard.NotNullOrEmpty(property, nameof(property)); - - return projectionClient.CreateProjectionAsync(property, string.Empty); - } - - public async Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default) - { - Guard.NotNull(callback, nameof(callback)); - Guard.NotNullOrEmpty(property, nameof(property)); - Guard.NotNull(value, nameof(value)); - - using (Profiler.TraceMethod()) - { - var streamName = await projectionClient.CreateProjectionAsync(property, value); - - var sliceStart = projectionClient.ParsePosition(position); - - await QueryAsync(callback, streamName, sliceStart, ct); - } - } - - public async Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default) - { - Guard.NotNull(callback, nameof(callback)); - - using (Profiler.TraceMethod()) - { - var streamName = await projectionClient.CreateProjectionAsync(streamFilter); - - var sliceStart = projectionClient.ParsePosition(position); - - await QueryAsync(callback, streamName, sliceStart, ct); - } - } - - private async Task QueryAsync(Func callback, string streamName, long sliceStart, CancellationToken ct = default) - { - StreamEventsSlice currentSlice; - do - { - currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, true); - - if (currentSlice.Status == SliceReadStatus.Success) - { - sliceStart = currentSlice.NextEventNumber; - - foreach (var resolved in currentSlice.Events) - { - var storedEvent = Formatter.Read(resolved, prefix, serializer); - - await callback(storedEvent); - } - } - } - while (!currentSlice.IsEndOfStream && !ct.IsCancellationRequested); - } - - public async Task> QueryAsync(string streamName, long streamPosition = 0) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - - using (Profiler.TraceMethod()) - { - var result = new List(); - - var sliceStart = streamPosition >= 0 ? streamPosition : StreamPosition.Start; - - StreamEventsSlice currentSlice; - do - { - currentSlice = await connection.ReadStreamEventsForwardAsync(GetStreamName(streamName), sliceStart, ReadPageSize, true); - - if (currentSlice.Status == SliceReadStatus.Success) - { - sliceStart = currentSlice.NextEventNumber; - - foreach (var resolved in currentSlice.Events) - { - var storedEvent = Formatter.Read(resolved, prefix, serializer); - - result.Add(storedEvent); - } - } - } - while (!currentSlice.IsEndOfStream); - - return result; - } - } - - public Task DeleteStreamAsync(string streamName) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - - return connection.DeleteStreamAsync(GetStreamName(streamName), ExpectedVersion.Any); - } - - public Task AppendAsync(Guid commitId, string streamName, ICollection events) - { - return AppendEventsInternalAsync(streamName, EtagVersion.Any, events); - } - - public Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) - { - Guard.GreaterEquals(expectedVersion, -1, nameof(expectedVersion)); - - return AppendEventsInternalAsync(streamName, expectedVersion, events); - } - - private async Task AppendEventsInternalAsync(string streamName, long expectedVersion, ICollection events) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNull(events, nameof(events)); - - using (Profiler.TraceMethod(nameof(AppendAsync))) - { - if (events.Count == 0) - { - return; - } - - try - { - var eventsToSave = events.Select(x => Formatter.Write(x, serializer)).ToList(); - - if (eventsToSave.Count < WritePageSize) - { - await connection.AppendToStreamAsync(GetStreamName(streamName), expectedVersion, eventsToSave); - } - else - { - using (var transaction = await connection.StartTransactionAsync(GetStreamName(streamName), expectedVersion)) - { - for (var p = 0; p < eventsToSave.Count; p += WritePageSize) - { - await transaction.WriteAsync(eventsToSave.Skip(p).Take(WritePageSize)); - } - - await transaction.CommitAsync(); - } - } - } - catch (WrongExpectedVersionException ex) - { - throw new WrongEventVersionException(ParseVersion(ex.Message), expectedVersion); - } - } - } - - private static int ParseVersion(string message) - { - return int.Parse(message.Substring(message.LastIndexOf(':') + 1)); - } - - private string GetStreamName(string streamName) - { - return $"{prefix}-{streamName}"; - } - } -} diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs deleted file mode 100644 index 0f06e4e77..000000000 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs +++ /dev/null @@ -1,81 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using EventStore.ClientAPI; -using EventStore.ClientAPI.Exceptions; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.EventSourcing -{ - internal sealed class GetEventStoreSubscription : IEventSubscription - { - private readonly IEventStoreConnection connection; - private readonly IEventSubscriber subscriber; - private readonly IJsonSerializer serializer; - private readonly string prefix; - private readonly EventStoreCatchUpSubscription subscription; - private readonly long? position; - - public GetEventStoreSubscription( - IEventStoreConnection connection, - IEventSubscriber subscriber, - IJsonSerializer serializer, - ProjectionClient projectionClient, - string position, - string prefix, - string streamFilter) - { - this.connection = connection; - - this.position = projectionClient.ParsePositionOrNull(position); - this.prefix = prefix; - - var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result; - - this.serializer = serializer; - this.subscriber = subscriber; - - subscription = SubscribeToStream(streamName); - } - - public Task StopAsync() - { - subscription.Stop(); - - return TaskHelper.Done; - } - - public void WakeUp() - { - } - - private EventStoreCatchUpSubscription SubscribeToStream(string streamName) - { - var settings = CatchUpSubscriptionSettings.Default; - - return connection.SubscribeToStreamFrom(streamName, position, settings, - (s, e) => - { - var storedEvent = Formatter.Read(e, prefix, serializer); - - subscriber.OnEventAsync(this, storedEvent).Wait(); - }, null, - (s, reason, ex) => - { - if (reason != SubscriptionDropReason.ConnectionClosed && - reason != SubscriptionDropReason.UserInitiated) - { - ex = ex ?? new ConnectionClosedException($"Subscription closed with reason {reason}."); - - subscriber.OnErrorAsync(this, ex); - } - }); - } - } -} diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs deleted file mode 100644 index ca098bb1e..000000000 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs +++ /dev/null @@ -1,143 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using EventStore.ClientAPI; -using EventStore.ClientAPI.Exceptions; -using EventStore.ClientAPI.Projections; - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class ProjectionClient - { - private readonly ConcurrentDictionary projections = new ConcurrentDictionary(); - private readonly IEventStoreConnection connection; - private readonly string prefix; - private readonly string projectionHost; - private ProjectionsManager projectionsManager; - - public ProjectionClient(IEventStoreConnection connection, string prefix, string projectionHost) - { - this.connection = connection; - - this.prefix = prefix; - this.projectionHost = projectionHost; - } - - private string CreateFilterProjectionName(string filter) - { - return $"by-{prefix.Slugify()}-{filter.Slugify()}"; - } - - private string CreatePropertyProjectionName(string property) - { - return $"by-{prefix.Slugify()}-{property.Slugify()}-property"; - } - - public async Task CreateProjectionAsync(string property, object value) - { - var name = CreatePropertyProjectionName(property); - - var query = - $@"fromAll() - .when({{ - $any: function (s, e) {{ - if (e.streamId.indexOf('{prefix}') === 0 && e.metadata.{property}) {{ - linkTo('{name}-' + e.metadata.{property}, e); - }} - }} - }});"; - - await CreateProjectionAsync(name, query); - - return $"{name}-{value}"; - } - - public async Task CreateProjectionAsync(string streamFilter = null) - { - streamFilter = streamFilter ?? ".*"; - - var name = CreateFilterProjectionName(streamFilter); - - var query = - $@"fromAll() - .when({{ - $any: function (s, e) {{ - if (e.streamId.indexOf('{prefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({prefix.Length + 1}))) {{ - linkTo('{name}', e); - }} - }} - }});"; - - await CreateProjectionAsync(name, query); - - return name; - } - - private async Task CreateProjectionAsync(string name, string query) - { - if (projections.TryAdd(name, true)) - { - try - { - var credentials = connection.Settings.DefaultUserCredentials; - - await projectionsManager.CreateContinuousAsync(name, query, credentials); - } - catch (Exception ex) - { - if (!ex.Is()) - { - throw; - } - } - } - } - - public async Task ConnectAsync(CancellationToken ct = default) - { - var addressParts = projectionHost.Split(':'); - - if (addressParts.Length < 2 || !int.TryParse(addressParts[1], out var port)) - { - port = 2113; - } - - var endpoints = await Dns.GetHostAddressesAsync(addressParts[0]); - var endpoint = new IPEndPoint(endpoints.First(x => x.AddressFamily == AddressFamily.InterNetwork), port); - - projectionsManager = - new ProjectionsManager( - connection.Settings.Log, endpoint, - connection.Settings.OperationTimeout); - try - { - await projectionsManager.ListAllAsync(connection.Settings.DefaultUserCredentials); - } - catch (Exception ex) - { - throw new ConfigurationException($"Cannot connect to event store projections: {projectionHost}.", ex); - } - } - - public long? ParsePositionOrNull(string position) - { - return long.TryParse(position, out var parsedPosition) ? (long?)parsedPosition : null; - } - - public long ParsePosition(string position) - { - return long.TryParse(position, out var parsedPosition) ? parsedPosition + 1 : StreamPosition.Start; - } - } -} diff --git a/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj b/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj deleted file mode 100644 index 8e213b530..000000000 --- a/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - netstandard2.0 - Squidex.Infrastructure - 7.3 - - - full - True - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs deleted file mode 100644 index ab8207c8b..000000000 --- a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs +++ /dev/null @@ -1,112 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Google; -using Google.Cloud.Storage.V1; - -namespace Squidex.Infrastructure.Assets -{ - public sealed class GoogleCloudAssetStore : IAssetStore, IInitializable - { - private static readonly UploadObjectOptions IfNotExists = new UploadObjectOptions { IfGenerationMatch = 0 }; - private static readonly CopyObjectOptions IfNotExistsCopy = new CopyObjectOptions { IfGenerationMatch = 0 }; - private readonly string bucketName; - private StorageClient storageClient; - - public GoogleCloudAssetStore(string bucketName) - { - Guard.NotNullOrEmpty(bucketName, nameof(bucketName)); - - this.bucketName = bucketName; - } - - public async Task InitializeAsync(CancellationToken ct = default) - { - try - { - storageClient = StorageClient.Create(); - - await storageClient.GetBucketAsync(bucketName, cancellationToken: ct); - } - catch (Exception ex) - { - throw new ConfigurationException($"Cannot connect to google cloud bucket '${bucketName}'.", ex); - } - } - - public string GeneratePublicUrl(string fileName) - { - return null; - } - - public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName)); - Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); - - try - { - await storageClient.CopyObjectAsync(bucketName, sourceFileName, bucketName, targetFileName, IfNotExistsCopy, ct); - } - catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) - { - throw new AssetNotFoundException(sourceFileName, ex); - } - catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.PreconditionFailed) - { - throw new AssetAlreadyExistsException(targetFileName); - } - } - - public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - try - { - await storageClient.DownloadObjectAsync(bucketName, fileName, stream, cancellationToken: ct); - } - catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) - { - throw new AssetNotFoundException(fileName, ex); - } - } - - public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - try - { - await storageClient.UploadObjectAsync(bucketName, fileName, "application/octet-stream", stream, overwrite ? null : IfNotExists, ct); - } - catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.PreconditionFailed) - { - throw new AssetAlreadyExistsException(fileName); - } - } - - public async Task DeleteAsync(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - try - { - await storageClient.DeleteObjectAsync(bucketName, fileName); - } - catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) - { - return; - } - } - } -} diff --git a/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj b/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj deleted file mode 100644 index 7151f2bde..000000000 --- a/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - netstandard2.0 - Squidex.Infrastructure - 7.3 - - - full - True - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs b/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs deleted file mode 100644 index cde15c0da..000000000 --- a/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs +++ /dev/null @@ -1,131 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using MongoDB.Driver.GridFS; - -namespace Squidex.Infrastructure.Assets -{ - public sealed class MongoGridFsAssetStore : IAssetStore, IInitializable - { - private const int BufferSize = 81920; - private readonly IGridFSBucket bucket; - - public MongoGridFsAssetStore(IGridFSBucket bucket) - { - Guard.NotNull(bucket, nameof(bucket)); - - this.bucket = bucket; - } - - public async Task InitializeAsync(CancellationToken ct = default) - { - try - { - await bucket.Database.ListCollectionsAsync(cancellationToken: ct); - } - catch (MongoException ex) - { - throw new ConfigurationException($"Cannot connect to Mongo GridFS bucket '${bucket.Options.BucketName}'.", ex); - } - } - - public string GeneratePublicUrl(string fileName) - { - return null; - } - - public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); - - try - { - var sourceName = GetFileName(sourceFileName, nameof(sourceFileName)); - - using (var readStream = await bucket.OpenDownloadStreamAsync(sourceFileName, cancellationToken: ct)) - { - await UploadAsync(targetFileName, readStream, false, ct); - } - } - catch (GridFSFileNotFoundException ex) - { - throw new AssetNotFoundException(sourceFileName, ex); - } - } - - public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) - { - Guard.NotNull(stream, nameof(stream)); - - try - { - var name = GetFileName(fileName, nameof(fileName)); - - using (var readStream = await bucket.OpenDownloadStreamAsync(name, cancellationToken: ct)) - { - await readStream.CopyToAsync(stream, BufferSize, ct); - } - } - catch (GridFSFileNotFoundException ex) - { - throw new AssetNotFoundException(fileName, ex); - } - } - - public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - Guard.NotNull(stream, nameof(stream)); - - try - { - var name = GetFileName(fileName, nameof(fileName)); - - if (overwrite) - { - await DeleteAsync(fileName); - } - - await bucket.UploadFromStreamAsync(fileName, fileName, stream, cancellationToken: ct); - } - catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) - { - throw new AssetAlreadyExistsException(fileName); - } - catch (MongoBulkWriteException ex) when (ex.WriteErrors.Any(x => x.Category == ServerErrorCategory.DuplicateKey)) - { - throw new AssetAlreadyExistsException(fileName); - } - } - - public async Task DeleteAsync(string fileName) - { - try - { - var name = GetFileName(fileName, nameof(fileName)); - - await bucket.DeleteAsync(name); - } - catch (GridFSFileNotFoundException) - { - return; - } - } - - private static string GetFileName(string fileName, string parameterName) - { - Guard.NotNullOrEmpty(fileName, parameterName); - - return fileName; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs b/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs deleted file mode 100644 index f4ce4efad..000000000 --- a/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using MongoDB.Driver; - -namespace Squidex.Infrastructure.Diagnostics -{ - public sealed class MongoDBHealthCheck : IHealthCheck - { - private readonly IMongoDatabase mongoDatabase; - - public MongoDBHealthCheck(IMongoDatabase mongoDatabase) - { - Guard.NotNull(mongoDatabase, nameof(mongoDatabase)); - - this.mongoDatabase = mongoDatabase; - } - - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - var collectionNames = await mongoDatabase.ListCollectionNamesAsync(cancellationToken: cancellationToken); - - var result = await collectionNames.AnyAsync(cancellationToken); - - var status = result ? HealthStatus.Healthy : HealthStatus.Unhealthy; - - return new HealthCheckResult(status, "Application must query data from MongoDB"); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs deleted file mode 100644 index c51dcfd1e..000000000 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Infrastructure.EventSourcing -{ - public partial class MongoEventStore : MongoRepositoryBase, IEventStore - { - private static readonly FieldDefinition TimestampField = Fields.Build(x => x.Timestamp); - private static readonly FieldDefinition EventsCountField = Fields.Build(x => x.EventsCount); - private static readonly FieldDefinition EventStreamOffsetField = Fields.Build(x => x.EventStreamOffset); - private static readonly FieldDefinition EventStreamField = Fields.Build(x => x.EventStream); - private readonly IEventNotifier notifier; - - public IMongoCollection RawCollection - { - get { return Database.GetCollection(CollectionName()); } - } - - public MongoEventStore(IMongoDatabase database, IEventNotifier notifier) - : base(database) - { - Guard.NotNull(notifier, nameof(notifier)); - - this.notifier = notifier; - } - - protected override string CollectionName() - { - return "Events"; - } - - protected override MongoCollectionSettings CollectionSettings() - { - return new MongoCollectionSettings { ReadPreference = ReadPreference.Primary, WriteConcern = WriteConcern.WMajority }; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index - .Ascending(x => x.Timestamp) - .Ascending(x => x.EventStream)), - new CreateIndexModel( - Index - .Ascending(x => x.EventStream) - .Descending(x => x.EventStreamOffset), - new CreateIndexOptions - { - Unique = true - }) - }, ct); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs deleted file mode 100644 index 0c03ebbe0..000000000 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs +++ /dev/null @@ -1,210 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.MongoDb; -using EventFilter = MongoDB.Driver.FilterDefinition; - -namespace Squidex.Infrastructure.EventSourcing -{ - public delegate bool EventPredicate(EventData data); - - public partial class MongoEventStore : MongoRepositoryBase, IEventStore - { - public Task CreateIndexAsync(string property) - { - Guard.NotNullOrEmpty(property, nameof(property)); - - return Collection.Indexes.CreateOneAsync( - new CreateIndexModel( - Index.Ascending(CreateIndexPath(property)))); - } - - public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null) - { - Guard.NotNull(subscriber, nameof(subscriber)); - - return new PollingSubscription(this, subscriber, streamFilter, position); - } - - public async Task> QueryAsync(string streamName, long streamPosition = 0) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - - using (Profiler.TraceMethod()) - { - var commits = - await Collection.Find( - Filter.And( - Filter.Eq(EventStreamField, streamName), - Filter.Gte(EventStreamOffsetField, streamPosition - MaxCommitSize))) - .Sort(Sort.Ascending(TimestampField)).ToListAsync(); - - var result = new List(); - - foreach (var commit in commits) - { - var eventStreamOffset = (int)commit.EventStreamOffset; - - var commitTimestamp = commit.Timestamp; - var commitOffset = 0; - - foreach (var @event in commit.Events) - { - eventStreamOffset++; - - if (eventStreamOffset >= streamPosition) - { - var eventData = @event.ToEventData(); - var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); - - result.Add(new StoredEvent(streamName, eventToken, eventStreamOffset, eventData)); - } - } - } - - return result; - } - } - - public Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default) - { - Guard.NotNull(callback, nameof(callback)); - Guard.NotNullOrEmpty(property, nameof(property)); - Guard.NotNull(value, nameof(value)); - - StreamPosition lastPosition = position; - - var filterDefinition = CreateFilter(property, value, lastPosition); - var filterExpression = CreateFilterExpression(property, value); - - return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); - } - - public Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default) - { - Guard.NotNull(callback, nameof(callback)); - - StreamPosition lastPosition = position; - - var filterDefinition = CreateFilter(streamFilter, lastPosition); - var filterExpression = CreateFilterExpression(null, null); - - return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); - } - - private async Task QueryAsync(Func callback, StreamPosition lastPosition, EventFilter filterDefinition, EventPredicate filterExpression, CancellationToken ct = default) - { - using (Profiler.TraceMethod()) - { - await Collection.Find(filterDefinition, options: Batching.Options).Sort(Sort.Ascending(TimestampField)).ForEachPipelineAsync(async commit => - { - var eventStreamOffset = (int)commit.EventStreamOffset; - - var commitTimestamp = commit.Timestamp; - var commitOffset = 0; - - foreach (var @event in commit.Events) - { - eventStreamOffset++; - - if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) - { - var eventData = @event.ToEventData(); - - if (filterExpression(eventData)) - { - var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); - - await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData)); - } - } - - commitOffset++; - } - }, ct); - } - } - - private static EventFilter CreateFilter(string property, object value, StreamPosition streamPosition) - { - var filters = new List(); - - AppendByPosition(streamPosition, filters); - AppendByProperty(property, value, filters); - - return Filter.And(filters); - } - - private static EventFilter CreateFilter(string streamFilter, StreamPosition streamPosition) - { - var filters = new List(); - - AppendByPosition(streamPosition, filters); - AppendByStream(streamFilter, filters); - - return Filter.And(filters); - } - - private static void AppendByProperty(string property, object value, List filters) - { - filters.Add(Filter.Eq(CreateIndexPath(property), value)); - } - - private static void AppendByStream(string streamFilter, List filters) - { - if (!StreamFilter.IsAll(streamFilter)) - { - if (streamFilter.Contains("^")) - { - filters.Add(Filter.Regex(EventStreamField, streamFilter)); - } - else - { - filters.Add(Filter.Eq(EventStreamField, streamFilter)); - } - } - } - - private static void AppendByPosition(StreamPosition streamPosition, List filters) - { - if (streamPosition.IsEndOfCommit) - { - filters.Add(Filter.Gt(TimestampField, streamPosition.Timestamp)); - } - else - { - filters.Add(Filter.Gte(TimestampField, streamPosition.Timestamp)); - } - } - - private static EventPredicate CreateFilterExpression(string property, object value) - { - if (!string.IsNullOrWhiteSpace(property)) - { - var jsonValue = JsonValue.Create(value); - - return x => x.Headers.TryGetValue(property, out var p) && p.Equals(jsonValue); - } - else - { - return x => true; - } - } - - private static string CreateIndexPath(string property) - { - return $"Events.Metadata.{property}"; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs deleted file mode 100644 index a18a836d2..000000000 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs +++ /dev/null @@ -1,144 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.EventSourcing -{ - public partial class MongoEventStore - { - private const int MaxCommitSize = 10; - private const int MaxWriteAttempts = 20; - private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0); - - public Task DeleteStreamAsync(string streamName) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - - return Collection.DeleteManyAsync(x => x.EventStream == streamName); - } - - public Task AppendAsync(Guid commitId, string streamName, ICollection events) - { - return AppendAsync(commitId, streamName, EtagVersion.Any, events); - } - - public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) - { - Guard.NotEmpty(commitId, nameof(commitId)); - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNull(events, nameof(events)); - Guard.LessThan(events.Count, MaxCommitSize, "events.Count"); - Guard.GreaterEquals(expectedVersion, EtagVersion.Any, nameof(expectedVersion)); - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNull(events, nameof(events)); - - using (Profiler.TraceMethod()) - { - if (events.Count == 0) - { - return; - } - - var currentVersion = await GetEventStreamOffsetAsync(streamName); - - if (expectedVersion > EtagVersion.Any && expectedVersion != currentVersion) - { - throw new WrongEventVersionException(currentVersion, expectedVersion); - } - - var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events); - - for (var attempt = 0; attempt < MaxWriteAttempts; attempt++) - { - try - { - await Collection.InsertOneAsync(commit); - - notifier.NotifyEventsStored(streamName); - - return; - } - catch (MongoWriteException ex) - { - if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) - { - currentVersion = await GetEventStreamOffsetAsync(streamName); - - if (expectedVersion > EtagVersion.Any) - { - throw new WrongEventVersionException(currentVersion, expectedVersion); - } - - if (attempt < MaxWriteAttempts) - { - expectedVersion = currentVersion; - } - else - { - throw new TimeoutException("Could not acquire a free slot for the commit within the provided time."); - } - } - else - { - throw; - } - } - } - } - } - - private async Task GetEventStreamOffsetAsync(string streamName) - { - var document = - await Collection.Find(Filter.Eq(EventStreamField, streamName)) - .Project(Projection - .Include(EventStreamOffsetField) - .Include(EventsCountField)) - .Sort(Sort.Descending(EventStreamOffsetField)).Limit(1) - .FirstOrDefaultAsync(); - - if (document != null) - { - return document[nameof(MongoEventCommit.EventStreamOffset)].ToInt64() + document[nameof(MongoEventCommit.EventsCount)].ToInt64(); - } - - return EtagVersion.Empty; - } - - private static MongoEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection events) - { - var commitEvents = new MongoEvent[events.Count]; - - var i = 0; - - foreach (var e in events) - { - var mongoEvent = MongoEvent.FromEventData(e); - - commitEvents[i++] = mongoEvent; - } - - var mongoCommit = new MongoEventCommit - { - Id = commitId, - Events = commitEvents, - EventsCount = events.Count, - EventStream = streamName, - EventStreamOffset = expectedVersion, - Timestamp = EmptyTimestamp - }; - - return mongoCommit; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs deleted file mode 100644 index 406abd90a..000000000 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using MongoDB.Bson; - -namespace Squidex.Infrastructure.EventSourcing -{ - internal sealed class StreamPosition - { - private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(946681200, 0); - - public BsonTimestamp Timestamp { get; } - - public long CommitOffset { get; } - - public long CommitSize { get; } - - public bool IsEndOfCommit - { - get { return CommitOffset == CommitSize - 1; } - } - - public StreamPosition(BsonTimestamp timestamp, long commitOffset, long commitSize) - { - Timestamp = timestamp; - - CommitOffset = commitOffset; - CommitSize = commitSize; - } - - public static implicit operator string(StreamPosition position) - { - var parts = new object[] - { - position.Timestamp.Timestamp, - position.Timestamp.Increment, - position.CommitOffset, - position.CommitSize - }; - - return string.Join("-", parts); - } - - public static implicit operator StreamPosition(string position) - { - if (!string.IsNullOrWhiteSpace(position)) - { - var parts = position.Split('-'); - - return new StreamPosition(new BsonTimestamp(int.Parse(parts[0]), int.Parse(parts[1])), long.Parse(parts[2]), long.Parse(parts[3])); - } - - return new StreamPosition(EmptyTimestamp, -1, -1); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs deleted file mode 100644 index 7fe870e3c..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Reflection; -using System.Threading; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Conventions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Squidex.Infrastructure.MongoDb -{ - public static class BsonJsonConvention - { - private static volatile int isRegistered; - - public static void Register(JsonSerializer serializer) - { - if (Interlocked.Increment(ref isRegistered) == 1) - { - var pack = new ConventionPack(); - - pack.AddMemberMapConvention("JsonBson", memberMap => - { - var attributes = memberMap.MemberInfo.GetCustomAttributes(); - - if (attributes.OfType().Any()) - { - var bsonSerializerType = typeof(BsonJsonSerializer<>).MakeGenericType(memberMap.MemberType); - var bsonSerializer = Activator.CreateInstance(bsonSerializerType, serializer); - - memberMap.SetSerializer((IBsonSerializer)bsonSerializer); - } - else if (memberMap.MemberType == typeof(JToken)) - { - memberMap.SetSerializer(JTokenSerializer.Instance); - } - else if (memberMap.MemberType == typeof(JObject)) - { - memberMap.SetSerializer(JTokenSerializer.Instance); - } - else if (memberMap.MemberType == typeof(JValue)) - { - memberMap.SetSerializer(JTokenSerializer.Instance); - } - }); - - ConventionRegistry.Register("json", pack, t => true); - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs deleted file mode 100644 index f801aeddc..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using MongoDB.Bson; -using MongoDB.Bson.IO; -using NewtonsoftJsonReader = Newtonsoft.Json.JsonReader; -using NewtonsoftJsonToken = Newtonsoft.Json.JsonToken; - -namespace Squidex.Infrastructure.MongoDb -{ - public sealed class BsonJsonReader : NewtonsoftJsonReader - { - private readonly IBsonReader bsonReader; - - public BsonJsonReader(IBsonReader bsonReader) - { - Guard.NotNull(bsonReader, nameof(bsonReader)); - - this.bsonReader = bsonReader; - } - - public override bool Read() - { - if (bsonReader.State == BsonReaderState.Initial || - bsonReader.State == BsonReaderState.ScopeDocument || - bsonReader.State == BsonReaderState.Type) - { - bsonReader.ReadBsonType(); - } - - if (bsonReader.State == BsonReaderState.Name) - { - SetToken(NewtonsoftJsonToken.PropertyName, bsonReader.ReadName().UnescapeBson()); - } - else if (bsonReader.State == BsonReaderState.Value) - { - switch (bsonReader.CurrentBsonType) - { - case BsonType.Document: - SetToken(NewtonsoftJsonToken.StartObject); - bsonReader.ReadStartDocument(); - break; - case BsonType.Array: - SetToken(NewtonsoftJsonToken.StartArray); - bsonReader.ReadStartArray(); - break; - case BsonType.Undefined: - SetToken(NewtonsoftJsonToken.Undefined); - bsonReader.ReadUndefined(); - break; - case BsonType.Null: - SetToken(NewtonsoftJsonToken.Null); - bsonReader.ReadNull(); - break; - case BsonType.String: - SetToken(NewtonsoftJsonToken.String, bsonReader.ReadString()); - break; - case BsonType.Binary: - SetToken(NewtonsoftJsonToken.Bytes, bsonReader.ReadBinaryData().Bytes); - break; - case BsonType.Boolean: - SetToken(NewtonsoftJsonToken.Boolean, bsonReader.ReadBoolean()); - break; - case BsonType.DateTime: - SetToken(NewtonsoftJsonToken.Date, bsonReader.ReadDateTime()); - break; - case BsonType.Int32: - SetToken(NewtonsoftJsonToken.Integer, bsonReader.ReadInt32()); - break; - case BsonType.Int64: - SetToken(NewtonsoftJsonToken.Integer, bsonReader.ReadInt64()); - break; - case BsonType.Double: - SetToken(NewtonsoftJsonToken.Float, bsonReader.ReadDouble()); - break; - case BsonType.Decimal128: - SetToken(NewtonsoftJsonToken.Float, Decimal128.ToDouble(bsonReader.ReadDecimal128())); - break; - default: - throw new NotSupportedException(); - } - } - else if (bsonReader.State == BsonReaderState.EndOfDocument) - { - SetToken(NewtonsoftJsonToken.EndObject); - bsonReader.ReadEndDocument(); - } - else if (bsonReader.State == BsonReaderState.EndOfArray) - { - SetToken(NewtonsoftJsonToken.EndArray); - bsonReader.ReadEndArray(); - } - - if (bsonReader.State == BsonReaderState.Initial) - { - return true; - } - - return !bsonReader.IsAtEndOfFile(); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs deleted file mode 100644 index eefb3e3e5..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; -using Newtonsoft.Json; - -namespace Squidex.Infrastructure.MongoDb -{ - public sealed class BsonJsonSerializer : ClassSerializerBase where T : class - { - private readonly JsonSerializer serializer; - - public BsonJsonSerializer(JsonSerializer serializer) - { - Guard.NotNull(serializer, nameof(serializer)); - - this.serializer = serializer; - } - - public override T Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) - { - var bsonReader = context.Reader; - - if (bsonReader.GetCurrentBsonType() == BsonType.Null) - { - bsonReader.ReadNull(); - - return null; - } - else - { - var jsonReader = new BsonJsonReader(bsonReader); - - return serializer.Deserialize(jsonReader); - } - } - - public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T value) - { - var bsonWriter = context.Writer; - - if (value == null) - { - bsonWriter.WriteNull(); - } - else - { - var jsonWriter = new BsonJsonWriter(bsonWriter); - - serializer.Serialize(jsonWriter, value); - } - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs deleted file mode 100644 index 558970951..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs +++ /dev/null @@ -1,178 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Globalization; -using MongoDB.Bson.IO; -using NewtonsoftJSonWriter = Newtonsoft.Json.JsonWriter; - -namespace Squidex.Infrastructure.MongoDb -{ - public sealed class BsonJsonWriter : NewtonsoftJSonWriter - { - private readonly IBsonWriter bsonWriter; - - public BsonJsonWriter(IBsonWriter bsonWriter) - { - Guard.NotNull(bsonWriter, nameof(bsonWriter)); - - this.bsonWriter = bsonWriter; - } - - public override void WritePropertyName(string name) - { - bsonWriter.WriteName(name.EscapeJson()); - } - - public override void WritePropertyName(string name, bool escape) - { - bsonWriter.WriteName(name.EscapeJson()); - } - - public override void WriteStartArray() - { - bsonWriter.WriteStartArray(); - } - - public override void WriteEndArray() - { - bsonWriter.WriteEndArray(); - } - - public override void WriteStartObject() - { - bsonWriter.WriteStartDocument(); - } - - public override void WriteEndObject() - { - bsonWriter.WriteEndDocument(); - } - - public override void WriteNull() - { - bsonWriter.WriteNull(); - } - - public override void WriteUndefined() - { - bsonWriter.WriteUndefined(); - } - - public override void WriteValue(string value) - { - bsonWriter.WriteString(value); - } - - public override void WriteValue(int value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(uint value) - { - bsonWriter.WriteInt32((int)value); - } - - public override void WriteValue(long value) - { - bsonWriter.WriteInt64(value); - } - - public override void WriteValue(ulong value) - { - bsonWriter.WriteInt64((long)value); - } - - public override void WriteValue(float value) - { - bsonWriter.WriteDouble(value); - } - - public override void WriteValue(double value) - { - bsonWriter.WriteDouble(value); - } - - public override void WriteValue(bool value) - { - bsonWriter.WriteBoolean(value); - } - - public override void WriteValue(short value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(ushort value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(char value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(byte value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(sbyte value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(decimal value) - { - bsonWriter.WriteDecimal128(value); - } - - public override void WriteValue(DateTime value) - { - bsonWriter.WriteString(value.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); - } - - public override void WriteValue(DateTimeOffset value) - { - if (value.Offset == TimeSpan.Zero) - { - bsonWriter.WriteString(value.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); - } - else - { - bsonWriter.WriteString(value.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); - } - } - - public override void WriteValue(byte[] value) - { - bsonWriter.WriteBytes(value); - } - - public override void WriteValue(TimeSpan value) - { - bsonWriter.WriteString(value.ToString()); - } - - public override void WriteValue(Guid value) - { - bsonWriter.WriteString(value.ToString()); - } - - public override void WriteValue(Uri value) - { - bsonWriter.WriteString(value.ToString()); - } - - public override void Flush() - { - bsonWriter.Flush(); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs deleted file mode 100644 index 755a4ae25..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; -using Newtonsoft.Json.Linq; - -namespace Squidex.Infrastructure.MongoDb -{ - public sealed class JTokenSerializer : ClassSerializerBase where T : JToken - { - public static readonly JTokenSerializer Instance = new JTokenSerializer(); - - public override T Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) - { - var bsonReader = context.Reader; - - if (bsonReader.GetCurrentBsonType() == BsonType.Null) - { - bsonReader.ReadNull(); - - return null; - } - else - { - var jsonReader = new BsonJsonReader(bsonReader); - - return (T)JToken.ReadFrom(jsonReader); - } - } - - public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T value) - { - var bsonWriter = context.Writer; - - if (value == null) - { - bsonWriter.WriteNull(); - } - else - { - var jsonWriter = new BsonJsonWriter(bsonWriter); - - value.WriteTo(jsonWriter); - } - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs deleted file mode 100644 index 0dc8cbf39..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs +++ /dev/null @@ -1,216 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Infrastructure.States; - -#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body - -namespace Squidex.Infrastructure.MongoDb -{ - public static class MongoExtensions - { - private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; - - public static async Task CollectionExistsAsync(this IMongoDatabase database, string collectionName) - { - var options = new ListCollectionNamesOptions - { - Filter = new BsonDocument("name", collectionName) - }; - - return (await database.ListCollectionNamesAsync(options)).Any(); - } - - public static async Task InsertOneIfNotExistsAsync(this IMongoCollection collection, T document) - { - try - { - await collection.InsertOneAsync(document); - } - catch (MongoWriteException ex) - { - if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) - { - return false; - } - - throw; - } - - return true; - } - - public static async Task TryDropOneAsync(this IMongoIndexManager indexes, string name) - { - try - { - await indexes.DropOneAsync(name); - } - catch - { - /* NOOP */ - } - } - - public static IFindFluent Only(this IFindFluent find, - Expression> include) - { - return find.Project(Builders.Projection.Include(include)); - } - - public static IFindFluent Only(this IFindFluent find, - Expression> include1, - Expression> include2) - { - return find.Project(Builders.Projection.Include(include1).Include(include2)); - } - - public static IFindFluent Only(this IFindFluent find, - Expression> include1, - Expression> include2, - Expression> include3) - { - return find.Project(Builders.Projection.Include(include1).Include(include2).Include(include3)); - } - - public static IFindFluent Not(this IFindFluent find, - Expression> exclude) - { - return find.Project(Builders.Projection.Exclude(exclude)); - } - - public static IFindFluent Not(this IFindFluent find, - Expression> exclude1, - Expression> exclude2) - { - return find.Project(Builders.Projection.Exclude(exclude1).Exclude(exclude2)); - } - - public static IFindFluent Not(this IFindFluent find, - Expression> exclude1, - Expression> exclude2, - Expression> exclude3) - { - return find.Project(Builders.Projection.Exclude(exclude1).Exclude(exclude2).Exclude(exclude3)); - } - - public static async Task UpsertVersionedAsync(this IMongoCollection collection, TKey key, long oldVersion, long newVersion, Func, UpdateDefinition> updater) where T : IVersionedEntity - { - try - { - var update = updater(Builders.Update.Set(x => x.Version, newVersion)); - - await collection.UpdateOneAsync(x => x.Id.Equals(key) && x.Version == oldVersion, update, Upsert); - } - catch (MongoWriteException ex) - { - if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) - { - var existingVersion = - await collection.Find(x => x.Id.Equals(key)).Only(x => x.Id, x => x.Version) - .FirstOrDefaultAsync(); - - if (existingVersion != null) - { - throw new InconsistentStateException(existingVersion[nameof(IVersionedEntity.Version)].AsInt64, oldVersion, ex); - } - } - else - { - throw; - } - } - } - - public static async Task UpsertVersionedAsync(this IMongoCollection collection, TKey key, long oldVersion, T doc) where T : IVersionedEntity - { - try - { - await collection.ReplaceOneAsync(x => x.Id.Equals(key) && x.Version == oldVersion, doc, Upsert); - } - catch (MongoWriteException ex) - { - if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) - { - var existingVersion = - await collection.Find(x => x.Id.Equals(key)).Only(x => x.Id, x => x.Version) - .FirstOrDefaultAsync(); - - if (existingVersion != null) - { - throw new InconsistentStateException(existingVersion[nameof(IVersionedEntity.Version)].AsInt64, oldVersion, ex); - } - } - else - { - throw; - } - } - } - - public static async Task ForEachPipelineAsync(this IAsyncCursorSource source, Func processor, CancellationToken cancellationToken = default) - { - using (var cursor = await source.ToCursorAsync(cancellationToken)) - { - await cursor.ForEachPipelineAsync(processor, cancellationToken); - } - } - - public static async Task ForEachPipelineAsync(this IAsyncCursor source, Func processor, CancellationToken cancellationToken = default) - { - using (var selfToken = new CancellationTokenSource()) - { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(selfToken.Token, cancellationToken)) - { - var actionBlock = - new ActionBlock(async x => - { - if (!combined.IsCancellationRequested) - { - await processor(x); - } - }, - new ExecutionDataflowBlockOptions - { - MaxDegreeOfParallelism = 1, - MaxMessagesPerTask = 1, - BoundedCapacity = Batching.BufferSize - }); - try - { - await source.ForEachAsync(async i => - { - var t = source; - - if (!await actionBlock.SendAsync(i, combined.Token)) - { - selfToken.Cancel(); - } - }, combined.Token); - - actionBlock.Complete(); - } - catch (Exception ex) - { - ((IDataflowBlock)actionBlock).Fault(ex); - } - finally - { - await actionBlock.Completion; - } - } - } - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs deleted file mode 100644 index afe2cf5f4..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Infrastructure.Tasks; - -#pragma warning disable RECS0108 // Warns about static fields in generic types - -namespace Squidex.Infrastructure.MongoDb -{ - public abstract class MongoRepositoryBase : IInitializable - { - private const string CollectionFormat = "{0}Set"; - - protected static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; - protected static readonly SortDefinitionBuilder Sort = Builders.Sort; - protected static readonly UpdateDefinitionBuilder Update = Builders.Update; - protected static readonly FieldDefinitionBuilder Fields = FieldDefinitionBuilder.Instance; - protected static readonly FilterDefinitionBuilder Filter = Builders.Filter; - protected static readonly IndexKeysDefinitionBuilder Index = Builders.IndexKeys; - protected static readonly ProjectionDefinitionBuilder Projection = Builders.Projection; - - private readonly IMongoDatabase mongoDatabase; - private Lazy> mongoCollection; - - protected IMongoCollection Collection - { - get { return mongoCollection.Value; } - } - - protected IMongoDatabase Database - { - get { return mongoDatabase; } - } - - static MongoRepositoryBase() - { - RefTokenSerializer.Register(); - - InstantSerializer.Register(); - } - - protected MongoRepositoryBase(IMongoDatabase database) - { - Guard.NotNull(database, nameof(database)); - - mongoDatabase = database; - mongoCollection = CreateCollection(); - } - - protected virtual MongoCollectionSettings CollectionSettings() - { - return new MongoCollectionSettings(); - } - - protected virtual string CollectionName() - { - return string.Format(CultureInfo.InvariantCulture, CollectionFormat, typeof(TEntity).Name); - } - - private Lazy> CreateCollection() - { - return new Lazy>(() => - mongoDatabase.GetCollection( - CollectionName(), - CollectionSettings() ?? new MongoCollectionSettings())); - } - - protected virtual Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - return TaskHelper.Done; - } - - public virtual async Task ClearAsync() - { - await Database.DropCollectionAsync(CollectionName()); - - await SetupCollectionAsync(Collection); - } - - public async Task InitializeAsync(CancellationToken ct = default) - { - try - { - await SetupCollectionAsync(Collection, ct); - } - catch (Exception ex) - { - throw new ConfigurationException($"MongoDb connection failed to connect to database {Database.DatabaseNamespace.DatabaseName}", ex); - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs deleted file mode 100644 index b89b8d028..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using MongoDB.Driver; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Infrastructure.MongoDb.Queries -{ - public static class FilterBuilder - { - public static (FilterDefinition Filter, bool Last) BuildFilter(this ClrQuery query, bool supportsSearch = true) - { - if (query.FullText != null) - { - if (!supportsSearch) - { - throw new ValidationException("Query $search clause not supported."); - } - - return (Builders.Filter.Text(query.FullText), false); - } - - if (query.Filter != null) - { - return (query.Filter.BuildFilter(), true); - } - - return (null, false); - } - - public static FilterDefinition BuildFilter(this FilterNode filterNode) - { - return FilterVisitor.Visit(filterNode); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs deleted file mode 100644 index c5a597b41..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs +++ /dev/null @@ -1,92 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Linq; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Infrastructure.MongoDb.Queries -{ - public sealed class FilterVisitor : FilterNodeVisitor, ClrValue> - { - private static readonly FilterDefinitionBuilder Filter = Builders.Filter; - private static readonly FilterVisitor Instance = new FilterVisitor(); - - private FilterVisitor() - { - } - - public static FilterDefinition Visit(FilterNode node) - { - return node.Accept(Instance); - } - - public override FilterDefinition Visit(NegateFilter nodeIn) - { - return Filter.Not(nodeIn.Filter.Accept(this)); - } - - public override FilterDefinition Visit(LogicalFilter nodeIn) - { - if (nodeIn.Type == LogicalFilterType.And) - { - return Filter.And(nodeIn.Filters.Select(x => x.Accept(this))); - } - else - { - return Filter.Or(nodeIn.Filters.Select(x => x.Accept(this))); - } - } - - public override FilterDefinition Visit(CompareFilter nodeIn) - { - var propertyName = nodeIn.Path.ToString(); - - var value = nodeIn.Value.Value; - - switch (nodeIn.Operator) - { - case CompareOperator.Empty: - return Filter.Or( - Filter.Exists(propertyName, false), - Filter.Eq(propertyName, default(T)), - Filter.Eq(propertyName, string.Empty), - Filter.Eq(propertyName, new T[0])); - case CompareOperator.StartsWith: - return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "^" + s)); - case CompareOperator.Contains: - return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s)); - case CompareOperator.EndsWith: - return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s + "$")); - case CompareOperator.Equals: - return Filter.Eq(propertyName, value); - case CompareOperator.GreaterThan: - return Filter.Gt(propertyName, value); - case CompareOperator.GreaterThanOrEqual: - return Filter.Gte(propertyName, value); - case CompareOperator.LessThan: - return Filter.Lt(propertyName, value); - case CompareOperator.LessThanOrEqual: - return Filter.Lte(propertyName, value); - case CompareOperator.NotEquals: - return Filter.Ne(propertyName, value); - case CompareOperator.In: - return Filter.In(propertyName, ((IList)value).OfType()); - } - - throw new NotSupportedException(); - } - - private static BsonRegularExpression BuildRegex(CompareFilter node, Func formatter) - { - return new BsonRegularExpression(formatter(node.Value.Value.ToString()), "i"); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs deleted file mode 100644 index 68b087166..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using MongoDB.Driver; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Infrastructure.MongoDb.Queries -{ - public static class SortBuilder - { - public static SortDefinition BuildSort(this ClrQuery query) - { - if (query.Sort.Count > 0) - { - var sorts = new List>(); - - foreach (var sort in query.Sort) - { - sorts.Add(OrderBy(sort)); - } - - if (sorts.Count > 1) - { - return Builders.Sort.Combine(sorts); - } - else - { - return sorts[0]; - } - } - - return null; - } - - public static SortDefinition OrderBy(SortNode sort) - { - var propertyName = string.Join(".", sort.Path); - - if (sort.Order == SortOrder.Ascending) - { - return Builders.Sort.Ascending(propertyName); - } - else - { - return Builders.Sort.Descending(propertyName); - } - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj deleted file mode 100644 index 3ce977368..000000000 --- a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - netstandard2.0 - Squidex.Infrastructure - 7.3 - - - full - True - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs deleted file mode 100644 index e9a7cc675..000000000 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Newtonsoft.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Infrastructure.States -{ - public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore - { - public MongoSnapshotStore(IMongoDatabase database, JsonSerializer jsonSerializer) - : base(database) - { - Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); - - BsonJsonConvention.Register(jsonSerializer); - } - - protected override string CollectionName() - { - var attribute = typeof(T).GetCustomAttributes(true).OfType().FirstOrDefault(); - - var name = attribute?.Name ?? typeof(T).Name; - - return $"States_{name}"; - } - - public async Task<(T Value, long Version)> ReadAsync(TKey key) - { - using (Profiler.TraceMethod>()) - { - var existing = - await Collection.Find(x => x.Id.Equals(key)) - .FirstOrDefaultAsync(); - - if (existing != null) - { - return (existing.Doc, existing.Version); - } - - return (default, EtagVersion.NotFound); - } - } - - public async Task WriteAsync(TKey key, T value, long oldVersion, long newVersion) - { - using (Profiler.TraceMethod>()) - { - await Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u.Set(x => x.Doc, value)); - } - } - - public async Task ReadAllAsync(Func callback, CancellationToken ct = default) - { - using (Profiler.TraceMethod>()) - { - await Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(x => callback(x.Doc, x.Version), ct); - } - } - - public async Task RemoveAsync(TKey key) - { - using (Profiler.TraceMethod>()) - { - await Collection.DeleteOneAsync(x => x.Id.Equals(key)); - } - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs b/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs deleted file mode 100644 index 421862d29..000000000 --- a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs +++ /dev/null @@ -1,105 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Infrastructure.UsageTracking -{ - public sealed class MongoUsageRepository : MongoRepositoryBase, IUsageRepository - { - private static readonly BulkWriteOptions Unordered = new BulkWriteOptions { IsOrdered = false }; - - public MongoUsageRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "UsagesV2"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - return collection.Indexes.CreateOneAsync( - new CreateIndexModel( - Index - .Ascending(x => x.Key) - .Ascending(x => x.Category) - .Ascending(x => x.Date)), - cancellationToken: ct); - } - - public async Task TrackUsagesAsync(UsageUpdate update) - { - Guard.NotNull(update, nameof(update)); - - if (update.Counters.Count > 0) - { - var (filter, updateStatement) = CreateOperation(update); - - await Collection.UpdateOneAsync(filter, updateStatement, Upsert); - } - } - - public async Task TrackUsagesAsync(params UsageUpdate[] updates) - { - if (updates.Length == 1) - { - await TrackUsagesAsync(updates[0]); - } - else if (updates.Length > 0) - { - var writes = new List>(); - - foreach (var update in updates) - { - if (update.Counters.Count > 0) - { - var (filter, updateStatement) = CreateOperation(update); - - writes.Add(new UpdateOneModel(filter, updateStatement) { IsUpsert = true }); - } - } - - await Collection.BulkWriteAsync(writes, Unordered); - } - } - - private static (FilterDefinition, UpdateDefinition) CreateOperation(UsageUpdate usageUpdate) - { - var id = $"{usageUpdate.Key}_{usageUpdate.Date:yyyy-MM-dd}_{usageUpdate.Category}"; - - var update = Update - .SetOnInsert(x => x.Key, usageUpdate.Key) - .SetOnInsert(x => x.Date, usageUpdate.Date) - .SetOnInsert(x => x.Category, usageUpdate.Category); - - foreach (var counter in usageUpdate.Counters) - { - update = update.Inc($"Counters.{counter.Key}", counter.Value); - } - - var filter = Filter.Eq(x => x.Id, id); - - return (filter, update); - } - - public async Task> QueryAsync(string key, DateTime fromDate, DateTime toDate) - { - var entities = await Collection.Find(x => x.Key == key && x.Date >= fromDate && x.Date <= toDate).ToListAsync(); - - return entities.Select(x => new StoredUsage(x.Category, x.Date, x.Counters)).ToList(); - } - } -} diff --git a/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs b/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs deleted file mode 100644 index 5e426c8fc..000000000 --- a/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs +++ /dev/null @@ -1,104 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using RabbitMQ.Client; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.CQRS.Events -{ - public sealed class RabbitMqEventConsumer : DisposableObjectBase, IInitializable, IEventConsumer - { - private readonly IJsonSerializer jsonSerializer; - private readonly string eventPublisherName; - private readonly string exchange; - private readonly string eventsFilter; - private readonly ConnectionFactory connectionFactory; - private readonly Lazy connection; - private readonly Lazy channel; - - public string Name - { - get { return eventPublisherName; } - } - - public string EventsFilter - { - get { return eventsFilter; } - } - - public RabbitMqEventConsumer(IJsonSerializer jsonSerializer, string eventPublisherName, string uri, string exchange, string eventsFilter) - { - Guard.NotNullOrEmpty(uri, nameof(uri)); - Guard.NotNullOrEmpty(eventPublisherName, nameof(eventPublisherName)); - Guard.NotNullOrEmpty(exchange, nameof(exchange)); - Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); - - connectionFactory = new ConnectionFactory { Uri = new Uri(uri, UriKind.Absolute) }; - connection = new Lazy(connectionFactory.CreateConnection); - channel = new Lazy(connection.Value.CreateModel); - - this.exchange = exchange; - this.eventsFilter = eventsFilter; - this.eventPublisherName = eventPublisherName; - this.jsonSerializer = jsonSerializer; - } - - protected override void DisposeObject(bool disposing) - { - if (connection.IsValueCreated) - { - connection.Value.Close(); - connection.Value.Dispose(); - } - } - - public Task InitializeAsync(CancellationToken ct = default) - { - try - { - var currentConnection = connection.Value; - - if (!currentConnection.IsOpen) - { - throw new ConfigurationException($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}"); - } - - return TaskHelper.Done; - } - catch (Exception e) - { - throw new ConfigurationException($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}", e); - } - } - - public bool Handles(StoredEvent @event) - { - return true; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - - public Task On(Envelope @event) - { - var jsonString = jsonSerializer.Serialize(@event); - var jsonBytes = Encoding.UTF8.GetBytes(jsonString); - - channel.Value.BasicPublish(exchange, string.Empty, null, jsonBytes); - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj b/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj deleted file mode 100644 index 53db2cb03..000000000 --- a/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - netstandard2.0 - Squidex.Infrastructure - 7.3 - - - full - True - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj b/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj deleted file mode 100644 index 5cf748e0a..000000000 --- a/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - netstandard2.0 - Squidex.Infrastructure - 7.3 - - - full - True - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs b/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs deleted file mode 100644 index ddf8465e0..000000000 --- a/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.Serialization; - -namespace Squidex.Infrastructure.Assets -{ - [Serializable] - public class AssetAlreadyExistsException : Exception - { - public AssetAlreadyExistsException(string fileName) - : base(FormatMessage(fileName)) - { - } - - public AssetAlreadyExistsException(string fileName, Exception inner) - : base(FormatMessage(fileName), inner) - { - } - - protected AssetAlreadyExistsException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - - private static string FormatMessage(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - return $"An asset with name '{fileName}' already exists."; - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/AssetFile.cs b/src/Squidex.Infrastructure/Assets/AssetFile.cs deleted file mode 100644 index 4f5ef010f..000000000 --- a/src/Squidex.Infrastructure/Assets/AssetFile.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; - -namespace Squidex.Infrastructure.Assets -{ - public sealed class AssetFile - { - private readonly Func openAction; - - public string FileName { get; } - - public string MimeType { get; } - - public long FileSize { get; } - - public AssetFile(string fileName, string mimeType, long fileSize, Func openAction) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - Guard.NotNullOrEmpty(mimeType, nameof(mimeType)); - Guard.GreaterEquals(fileSize, 0, nameof(fileSize)); - - FileName = fileName; - FileSize = fileSize; - - MimeType = mimeType; - - this.openAction = openAction; - } - - public Stream OpenRead() - { - return openAction(); - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs b/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs deleted file mode 100644 index 1691a8bbf..000000000 --- a/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.Serialization; - -namespace Squidex.Infrastructure.Assets -{ - [Serializable] - public class AssetNotFoundException : Exception - { - public AssetNotFoundException(string fileName) - : base(FormatMessage(fileName)) - { - } - - public AssetNotFoundException(string fileName, Exception inner) - : base(FormatMessage(fileName), inner) - { - } - - protected AssetNotFoundException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - - private static string FormatMessage(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - return $"An asset with name '{fileName}' does not exist."; - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/AssetStoreExtensions.cs b/src/Squidex.Infrastructure/Assets/AssetStoreExtensions.cs deleted file mode 100644 index a8824c314..000000000 --- a/src/Squidex.Infrastructure/Assets/AssetStoreExtensions.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Assets -{ - public static class AssetStoreExtensions - { - public static string GeneratePublicUrl(this IAssetStore store, Guid id, long version, string suffix) - { - return store.GeneratePublicUrl(id.ToString(), version, suffix); - } - - public static string GeneratePublicUrl(this IAssetStore store, string id, long version, string suffix) - { - return store.GeneratePublicUrl(GetFileName(id, version, suffix)); - } - - public static Task CopyAsync(this IAssetStore store, string sourceFileName, Guid id, long version, string suffix, CancellationToken ct = default) - { - return store.CopyAsync(sourceFileName, id.ToString(), version, suffix, ct); - } - - public static Task CopyAsync(this IAssetStore store, string sourceFileName, string id, long version, string suffix, CancellationToken ct = default) - { - return store.CopyAsync(sourceFileName, GetFileName(id, version, suffix), ct); - } - - public static Task DownloadAsync(this IAssetStore store, Guid id, long version, string suffix, Stream stream, CancellationToken ct = default) - { - return store.DownloadAsync(id.ToString(), version, suffix, stream, ct); - } - - public static Task DownloadAsync(this IAssetStore store, string id, long version, string suffix, Stream stream, CancellationToken ct = default) - { - return store.DownloadAsync(GetFileName(id, version, suffix), stream, ct); - } - - public static Task UploadAsync(this IAssetStore store, Guid id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - return store.UploadAsync(id.ToString(), version, suffix, stream, overwrite, ct); - } - - public static Task UploadAsync(this IAssetStore store, string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - return store.UploadAsync(GetFileName(id, version, suffix), stream, overwrite, ct); - } - - public static Task DeleteAsync(this IAssetStore store, Guid id, long version, string suffix) - { - return store.DeleteAsync(id.ToString(), version, suffix); - } - - public static Task DeleteAsync(this IAssetStore store, string id, long version, string suffix) - { - return store.DeleteAsync(GetFileName(id, version, suffix)); - } - - public static string GetFileName(string id, long version, string suffix = null) - { - Guard.NotNullOrEmpty(id, nameof(id)); - - return StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix); - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs b/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs deleted file mode 100644 index 0c417fe83..000000000 --- a/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs +++ /dev/null @@ -1,158 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using FluentFTP; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.Assets -{ - public sealed class FTPAssetStore : IAssetStore, IInitializable - { - private readonly string path; - private readonly ISemanticLog log; - private readonly Func factory; - - public FTPAssetStore(Func factory, string path, ISemanticLog log) - { - Guard.NotNull(factory, nameof(factory)); - Guard.NotNullOrEmpty(path, nameof(path)); - Guard.NotNull(log, nameof(log)); - - this.factory = factory; - this.path = path; - - this.log = log; - } - - public string GeneratePublicUrl(string fileName) - { - return null; - } - - public async Task InitializeAsync(CancellationToken ct = default) - { - using (var client = factory()) - { - await client.ConnectAsync(ct); - - if (!await client.DirectoryExistsAsync(path, ct)) - { - await client.CreateDirectoryAsync(path, ct); - } - } - - log.LogInformation(w => w - .WriteProperty("action", "FTPAssetStoreConfigured") - .WriteProperty("path", path)); - } - - public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName)); - Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); - - using (var client = GetFtpClient()) - { - var tempPath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); - - using (var stream = new FileStream(tempPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose)) - { - await DownloadAsync(client, sourceFileName, stream, ct); - await UploadAsync(client, targetFileName, stream, false, ct); - } - } - } - - public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - Guard.NotNull(stream, nameof(stream)); - - using (var client = GetFtpClient()) - { - await DownloadAsync(client, fileName, stream, ct); - } - } - - public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - Guard.NotNull(stream, nameof(stream)); - - using (var client = GetFtpClient()) - { - await UploadAsync(client, fileName, stream, overwrite, ct); - } - } - - private static async Task DownloadAsync(IFtpClient client, string fileName, Stream stream, CancellationToken ct) - { - try - { - await client.DownloadAsync(stream, fileName, token: ct); - } - catch (FtpException ex) when (IsNotFound(ex)) - { - throw new AssetNotFoundException(fileName, ex); - } - } - - private static async Task UploadAsync(IFtpClient client, string fileName, Stream stream, bool overwrite, CancellationToken ct) - { - if (!overwrite && await client.FileExistsAsync(fileName, ct)) - { - throw new AssetAlreadyExistsException(fileName); - } - - await client.UploadAsync(stream, fileName, overwrite ? FtpExists.Overwrite : FtpExists.Skip, true, null, ct); - } - - public async Task DeleteAsync(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - using (var client = GetFtpClient()) - { - try - { - await client.DeleteFileAsync(fileName); - } - catch (FtpException ex) - { - if (!IsNotFound(ex)) - { - throw; - } - } - } - } - - private IFtpClient GetFtpClient() - { - var client = factory(); - - client.Connect(); - client.SetWorkingDirectory(path); - - return client; - } - - private static bool IsNotFound(Exception exception) - { - if (exception is FtpCommandException command) - { - return command.CompletionCode == "550"; - } - - return exception.InnerException != null && IsNotFound(exception.InnerException); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs deleted file mode 100644 index c9a3e83f0..000000000 --- a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs +++ /dev/null @@ -1,142 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Assets -{ - public sealed class FolderAssetStore : IAssetStore, IInitializable - { - private const int BufferSize = 81920; - private readonly ISemanticLog log; - private readonly DirectoryInfo directory; - - public FolderAssetStore(string path, ISemanticLog log) - { - Guard.NotNullOrEmpty(path, nameof(path)); - Guard.NotNull(log, nameof(log)); - - this.log = log; - - directory = new DirectoryInfo(path); - } - - public Task InitializeAsync(CancellationToken ct = default) - { - try - { - if (!directory.Exists) - { - directory.Create(); - } - - log.LogInformation(w => w - .WriteProperty("action", "FolderAssetStoreConfigured") - .WriteProperty("path", directory.FullName)); - - return TaskHelper.Done; - } - catch (Exception ex) - { - throw new ConfigurationException($"Cannot access directory {directory.FullName}", ex); - } - } - - public string GeneratePublicUrl(string fileName) - { - return null; - } - - public Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName)); - Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); - - var targetFile = GetFile(targetFileName); - var sourceFile = GetFile(sourceFileName); - - try - { - sourceFile.CopyTo(targetFile.FullName); - - return TaskHelper.Done; - } - catch (IOException) when (targetFile.Exists) - { - throw new AssetAlreadyExistsException(targetFileName); - } - catch (FileNotFoundException ex) - { - throw new AssetNotFoundException(sourceFileName, ex); - } - } - - public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) - { - Guard.NotNull(stream, nameof(stream)); - - var file = GetFile(fileName); - - try - { - using (var fileStream = file.OpenRead()) - { - await fileStream.CopyToAsync(stream, BufferSize, ct); - } - } - catch (FileNotFoundException ex) - { - throw new AssetNotFoundException(fileName, ex); - } - } - - public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - Guard.NotNull(stream, nameof(stream)); - - var file = GetFile(fileName); - - try - { - using (var fileStream = file.Open(overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.Write)) - { - await stream.CopyToAsync(fileStream, BufferSize, ct); - } - } - catch (IOException) when (file.Exists) - { - throw new AssetAlreadyExistsException(file.Name); - } - } - - public Task DeleteAsync(string fileName) - { - var file = GetFile(fileName); - - file.Delete(); - - return TaskHelper.Done; - } - - private FileInfo GetFile(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - return new FileInfo(GetPath(fileName)); - } - - private string GetPath(string name) - { - return Path.Combine(directory.FullName, name); - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/HasherStream.cs b/src/Squidex.Infrastructure/Assets/HasherStream.cs deleted file mode 100644 index ea11e8682..000000000 --- a/src/Squidex.Infrastructure/Assets/HasherStream.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Security.Cryptography; - -namespace Squidex.Infrastructure.Assets -{ - public sealed class HasherStream : Stream - { - private readonly Stream inner; - private readonly IncrementalHash hasher; - - public override bool CanRead - { - get { return inner.CanRead; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanWrite - { - get { return false; } - } - - public override long Length - { - get { return inner.Length; } - } - - public override long Position - { - get { return inner.Position; } - set { throw new NotSupportedException(); } - } - - public HasherStream(Stream inner, HashAlgorithmName hashAlgorithmName) - { - Guard.NotNull(inner, nameof(inner)); - - this.inner = inner; - - hasher = IncrementalHash.CreateHash(hashAlgorithmName); - } - - public override int Read(byte[] buffer, int offset, int count) - { - var read = inner.Read(buffer, offset, count); - - if (read > 0) - { - hasher.AppendData(buffer, offset, read); - } - - return read; - } - - public byte[] GetHashAndReset() - { - return hasher.GetHashAndReset(); - } - - public string GetHashStringAndReset() - { - return Convert.ToBase64String(GetHashAndReset()); - } - - public override void Flush() - { - throw new NotSupportedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/IAssetStore.cs b/src/Squidex.Infrastructure/Assets/IAssetStore.cs deleted file mode 100644 index 207a626f8..000000000 --- a/src/Squidex.Infrastructure/Assets/IAssetStore.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Assets -{ - public interface IAssetStore - { - string GeneratePublicUrl(string fileName); - - Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default); - - Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default); - - Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default); - - Task DeleteAsync(string fileName); - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs b/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs deleted file mode 100644 index d4e46c533..000000000 --- a/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.IO; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Assets -{ - public interface IAssetThumbnailGenerator - { - Task GetImageInfoAsync(Stream source); - - Task CreateThumbnailAsync(Stream source, Stream destination, int? width = null, int? height = null, string mode = null, int? quality = null); - } -} diff --git a/src/Squidex.Infrastructure/Assets/ImageInfo.cs b/src/Squidex.Infrastructure/Assets/ImageInfo.cs deleted file mode 100644 index 2b3114cf3..000000000 --- a/src/Squidex.Infrastructure/Assets/ImageInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Assets -{ - public sealed class ImageInfo - { - public int PixelWidth { get; } - - public int PixelHeight { get; } - - public ImageInfo(int pixelWidth, int pixelHeight) - { - Guard.GreaterThan(pixelWidth, 0, nameof(pixelWidth)); - Guard.GreaterThan(pixelHeight, 0, nameof(pixelHeight)); - - PixelWidth = pixelWidth; - PixelHeight = pixelHeight; - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs b/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs deleted file mode 100644 index 929acb63d..000000000 --- a/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs +++ /dev/null @@ -1,95 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Transforms; -using SixLabors.Primitives; - -namespace Squidex.Infrastructure.Assets.ImageSharp -{ - public sealed class ImageSharpAssetThumbnailGenerator : IAssetThumbnailGenerator - { - public Task CreateThumbnailAsync(Stream source, Stream destination, int? width = null, int? height = null, string mode = null, int? quality = null) - { - return Task.Run(() => - { - if (!width.HasValue && !height.HasValue && !quality.HasValue) - { - source.CopyTo(destination); - - return; - } - - using (var sourceImage = Image.Load(source, out var format)) - { - var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format); - - if (quality.HasValue) - { - encoder = new JpegEncoder { Quality = quality.Value }; - } - - if (encoder == null) - { - throw new NotSupportedException(); - } - - if (width.HasValue || height.HasValue) - { - var isCropUpsize = string.Equals("CropUpsize", mode, StringComparison.OrdinalIgnoreCase); - - if (!Enum.TryParse(mode, true, out var resizeMode)) - { - resizeMode = ResizeMode.Max; - } - - if (isCropUpsize) - { - resizeMode = ResizeMode.Crop; - } - - var resizeWidth = width ?? 0; - var resizeHeight = height ?? 0; - - if (resizeWidth >= sourceImage.Width && resizeHeight >= sourceImage.Height && resizeMode == ResizeMode.Crop && !isCropUpsize) - { - resizeMode = ResizeMode.BoxPad; - } - - var options = new ResizeOptions { Size = new Size(resizeWidth, resizeHeight), Mode = resizeMode }; - - sourceImage.Mutate(x => x.Resize(options)); - } - - sourceImage.Save(destination, encoder); - } - }); - } - - public Task GetImageInfoAsync(Stream source) - { - return Task.Run(() => - { - try - { - var image = Image.Load(source); - - return new ImageInfo(image.Width, image.Height); - } - catch - { - return null; - } - }); - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs b/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs deleted file mode 100644 index 20d9fc363..000000000 --- a/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Concurrent; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Assets -{ - public class MemoryAssetStore : IAssetStore - { - private readonly ConcurrentDictionary streams = new ConcurrentDictionary(); - private readonly AsyncLock readerLock = new AsyncLock(); - private readonly AsyncLock writerLock = new AsyncLock(); - - public string GeneratePublicUrl(string fileName) - { - return null; - } - - public virtual async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName)); - Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); - - if (!streams.TryGetValue(sourceFileName, out var sourceStream)) - { - throw new AssetNotFoundException(sourceFileName); - } - - using (await readerLock.LockAsync()) - { - await UploadAsync(targetFileName, sourceStream, false, ct); - } - } - - public virtual async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - Guard.NotNull(stream, nameof(stream)); - - if (!streams.TryGetValue(fileName, out var sourceStream)) - { - throw new AssetNotFoundException(fileName); - } - - using (await readerLock.LockAsync()) - { - try - { - await sourceStream.CopyToAsync(stream, 81920, ct); - } - finally - { - sourceStream.Position = 0; - } - } - } - - public virtual async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - Guard.NotNull(stream, nameof(stream)); - - var memoryStream = new MemoryStream(); - - async Task CopyAsync() - { - using (await writerLock.LockAsync()) - { - try - { - await stream.CopyToAsync(memoryStream, 81920, ct); - } - finally - { - memoryStream.Position = 0; - } - } - } - - if (overwrite) - { - await CopyAsync(); - - streams[fileName] = memoryStream; - } - else if (streams.TryAdd(fileName, memoryStream)) - { - await CopyAsync(); - } - else - { - throw new AssetAlreadyExistsException(fileName); - } - } - - public virtual Task DeleteAsync(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - streams.TryRemove(fileName, out _); - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs b/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs deleted file mode 100644 index 85ccd58c9..000000000 --- a/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Assets -{ - public sealed class NoopAssetStore : IAssetStore - { - public string GeneratePublicUrl(string fileName) - { - return null; - } - - public Task CopyAsync(string sourceFileName, string fileName, CancellationToken ct = default) - { - throw new NotSupportedException(); - } - - public Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) - { - throw new NotSupportedException(); - } - - public Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - throw new NotSupportedException(); - } - - public Task DeleteAsync(string fileName) - { - throw new NotSupportedException(); - } - } -} diff --git a/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs b/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs deleted file mode 100644 index e6222aa37..000000000 --- a/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Threading; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Caching -{ - public sealed class AsyncLocalCache : ILocalCache - { - private static readonly AsyncLocal> LocalCache = new AsyncLocal>(); - private static readonly AsyncLocalCleaner> Cleaner; - - static AsyncLocalCache() - { - Cleaner = new AsyncLocalCleaner>(LocalCache); - } - - public IDisposable StartContext() - { - LocalCache.Value = new ConcurrentDictionary(); - - return Cleaner; - } - - public void Add(object key, object value) - { - var cacheKey = GetCacheKey(key); - - var cache = LocalCache.Value; - - if (cache != null) - { - cache[cacheKey] = value; - } - } - - public void Remove(object key) - { - var cacheKey = GetCacheKey(key); - - var cache = LocalCache.Value; - - if (cache != null) - { - cache.TryRemove(cacheKey, out _); - } - } - - public bool TryGetValue(object key, out object value) - { - var cacheKey = GetCacheKey(key); - - var cache = LocalCache.Value; - - if (cache != null) - { - return cache.TryGetValue(cacheKey, out value); - } - - value = null; - - return false; - } - - private static string GetCacheKey(object key) - { - return $"CACHE_{key}"; - } - } -} diff --git a/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs b/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs deleted file mode 100644 index 9ff773e51..000000000 --- a/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.Caching.Memory; - -namespace Squidex.Infrastructure.Caching -{ - public abstract class CachingProviderBase - { - private readonly IMemoryCache cache; - - protected IMemoryCache Cache - { - get { return cache; } - } - - protected CachingProviderBase(IMemoryCache cache) - { - Guard.NotNull(cache, nameof(cache)); - - this.cache = cache; - } - } -} diff --git a/src/Squidex.Infrastructure/Caching/ILocalCache.cs b/src/Squidex.Infrastructure/Caching/ILocalCache.cs deleted file mode 100644 index 5eec26296..000000000 --- a/src/Squidex.Infrastructure/Caching/ILocalCache.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Caching -{ - public interface ILocalCache - { - IDisposable StartContext(); - - void Add(object key, object value); - - void Remove(object key); - - bool TryGetValue(object key, out object value); - } -} diff --git a/src/Squidex.Infrastructure/Caching/LRUCache.cs b/src/Squidex.Infrastructure/Caching/LRUCache.cs deleted file mode 100644 index 98f9c10f3..000000000 --- a/src/Squidex.Infrastructure/Caching/LRUCache.cs +++ /dev/null @@ -1,103 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; - -namespace Squidex.Infrastructure.Caching -{ - public sealed class LRUCache - { - private readonly Dictionary>> cacheMap = new Dictionary>>(); - private readonly LinkedList> cacheHistory = new LinkedList>(); - private readonly int capacity; - private readonly Action itemEvicted; - - public LRUCache(int capacity, Action itemEvicted = null) - { - Guard.GreaterThan(capacity, 0, nameof(capacity)); - - this.capacity = capacity; - - this.itemEvicted = itemEvicted ?? ((key, value) => { }); - } - - public bool Set(TKey key, TValue value) - { - if (cacheMap.TryGetValue(key, out var node)) - { - node.Value.Value = value; - - cacheHistory.Remove(node); - cacheHistory.AddLast(node); - - cacheMap[key] = node; - - return true; - } - - if (cacheMap.Count >= capacity) - { - RemoveFirst(); - } - - var cacheItem = new LRUCacheItem { Key = key, Value = value }; - - node = new LinkedListNode>(cacheItem); - - cacheMap.Add(key, node); - cacheHistory.AddLast(node); - - return false; - } - - public bool Remove(TKey key) - { - if (cacheMap.TryGetValue(key, out var node)) - { - cacheMap.Remove(key); - cacheHistory.Remove(node); - - return true; - } - - return false; - } - - public bool TryGetValue(TKey key, out object value) - { - value = null; - - if (cacheMap.TryGetValue(key, out var node)) - { - value = node.Value.Value; - - cacheHistory.Remove(node); - cacheHistory.AddLast(node); - - return true; - } - - return false; - } - - public bool Contains(TKey key) - { - return cacheMap.ContainsKey(key); - } - - private void RemoveFirst() - { - var node = cacheHistory.First; - - itemEvicted(node.Value.Key, node.Value.Value); - - cacheMap.Remove(node.Value.Key); - cacheHistory.RemoveFirst(); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs b/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs deleted file mode 100644 index ff9cb3eef..000000000 --- a/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -#pragma warning disable SA1401 // Fields must be private - -namespace Squidex.Infrastructure.Caching -{ - internal class LRUCacheItem - { - public TKey Key; - - public TValue Value; - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/CollectionExtensions.cs b/src/Squidex.Infrastructure/CollectionExtensions.cs deleted file mode 100644 index f5b12cc82..000000000 --- a/src/Squidex.Infrastructure/CollectionExtensions.cs +++ /dev/null @@ -1,244 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Squidex.Infrastructure -{ - public static class CollectionExtensions - { - public static IResultList SortSet(this IResultList input, Func idProvider, IReadOnlyList ids) where T : class - { - return ResultList.Create(input.Total, SortList(input, idProvider, ids)); - } - - public static IEnumerable SortList(this IEnumerable input, Func idProvider, IReadOnlyList ids) where T : class - { - return ids.Select(id => input.FirstOrDefault(x => Equals(idProvider(x), id))).Where(x => x != null); - } - - public static void AddRange(this ICollection target, IEnumerable source) - { - foreach (var value in source) - { - target.Add(value); - } - } - - public static IEnumerable Shuffle(this IEnumerable enumerable) - { - var random = new Random(); - - return enumerable.OrderBy(x => random.Next()).ToList(); - } - - public static HashSet ToHashSet(this IEnumerable enumerable) - { - return new HashSet(enumerable); - } - - public static HashSet ToHashSet(this IEnumerable enumerable, IEqualityComparer comparer) - { - return new HashSet(enumerable, comparer); - } - - public static IEnumerable OrEmpty(this IEnumerable source) - { - return source ?? Enumerable.Empty(); - } - - public static IEnumerable Concat(this IEnumerable source, T value) - { - return source.Concat(Enumerable.Repeat(value, 1)); - } - - public static TResult[] Map(this T[] value, Func convert) - { - var result = new TResult[value.Length]; - - for (var i = 0; i < value.Length; i++) - { - result[i] = convert(value[i]); - } - - return result; - } - - public static int SequentialHashCode(this IEnumerable collection) - { - return collection.SequentialHashCode(EqualityComparer.Default); - } - - public static int SequentialHashCode(this IEnumerable collection, IEqualityComparer comparer) - { - var hashCode = 17; - - foreach (var item in collection) - { - if (!Equals(item, null)) - { - hashCode = (hashCode * 23) + comparer.GetHashCode(item); - } - } - - return hashCode; - } - - public static int OrderedHashCode(this IEnumerable collection) - { - return collection.OrderedHashCode(EqualityComparer.Default); - } - - public static int OrderedHashCode(this IEnumerable collection, IEqualityComparer comparer) - { - Guard.NotNull(comparer, nameof(comparer)); - - var hashCodes = collection.Where(x => !Equals(x, null)).Select(x => x.GetHashCode()).OrderBy(x => x).ToArray(); - - var hashCode = 17; - - foreach (var code in hashCodes) - { - hashCode = (hashCode * 23) + code; - } - - return hashCode; - } - - public static int DictionaryHashCode(this IDictionary dictionary) - { - return DictionaryHashCode(dictionary, EqualityComparer.Default, EqualityComparer.Default); - } - - public static int DictionaryHashCode(this IDictionary dictionary, IEqualityComparer keyComparer, IEqualityComparer valueComparer) - { - var hashCode = 17; - - foreach (var kvp in dictionary.OrderBy(x => x.Key)) - { - hashCode = (hashCode * 23) + keyComparer.GetHashCode(kvp.Key); - - if (!Equals(kvp.Value, null)) - { - hashCode = (hashCode * 23) + valueComparer.GetHashCode(kvp.Value); - } - } - - return hashCode; - } - - public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary other) - { - return EqualsDictionary(dictionary, other, EqualityComparer.Default, EqualityComparer.Default); - } - - public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary other, IEqualityComparer keyComparer, IEqualityComparer valueComparer) - { - var comparer = new KeyValuePairComparer(keyComparer, valueComparer); - - return other != null && dictionary.Count == other.Count && !dictionary.Except(other, comparer).Any(); - } - - public static TValue GetOrDefault(this IReadOnlyDictionary dictionary, TKey key) - { - return dictionary.GetOrCreate(key, _ => default); - } - - public static TValue GetOrAddDefault(this IDictionary dictionary, TKey key) - { - return dictionary.GetOrAdd(key, _ => default); - } - - public static TValue GetOrNew(this IReadOnlyDictionary dictionary, TKey key) where TValue : class, new() - { - return dictionary.GetOrCreate(key, _ => new TValue()); - } - - public static TValue GetOrAddNew(this IDictionary dictionary, TKey key) where TValue : class, new() - { - return dictionary.GetOrAdd(key, _ => new TValue()); - } - - public static TValue GetOrCreate(this IReadOnlyDictionary dictionary, TKey key, Func creator) - { - if (!dictionary.TryGetValue(key, out var result)) - { - result = creator(key); - } - - return result; - } - - public static TValue GetOrAdd(this IDictionary dictionary, TKey key, TValue fallback) - { - if (!dictionary.TryGetValue(key, out var result)) - { - result = fallback; - - dictionary.Add(key, result); - } - - return result; - } - - public static TValue GetOrAdd(this IDictionary dictionary, TKey key, Func creator) - { - if (!dictionary.TryGetValue(key, out var result)) - { - result = creator(key); - - dictionary.Add(key, result); - } - - return result; - } - - public static TValue GetOrAdd(this IDictionary dictionary, TKey key, TContext context, Func creator) - { - if (!dictionary.TryGetValue(key, out var result)) - { - result = creator(key, context); - - dictionary.Add(key, result); - } - - return result; - } - - public static void Foreach(this IEnumerable collection, Action action) - { - foreach (var item in collection) - { - action(item); - } - } - - public sealed class KeyValuePairComparer : IEqualityComparer> - { - private readonly IEqualityComparer keyComparer; - private readonly IEqualityComparer valueComparer; - - public KeyValuePairComparer(IEqualityComparer keyComparer, IEqualityComparer valueComparer) - { - this.keyComparer = keyComparer; - this.valueComparer = valueComparer; - } - - public bool Equals(KeyValuePair x, KeyValuePair y) - { - return keyComparer.Equals(x.Key, y.Key) && valueComparer.Equals(x.Value, y.Value); - } - - public int GetHashCode(KeyValuePair obj) - { - return keyComparer.GetHashCode(obj.Key) ^ valueComparer.GetHashCode(obj.Value); - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Collections/ArrayDictionary.cs b/src/Squidex.Infrastructure/Collections/ArrayDictionary.cs deleted file mode 100644 index 1a6f1afd2..000000000 --- a/src/Squidex.Infrastructure/Collections/ArrayDictionary.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Squidex.Infrastructure.Collections -{ - public static class ArrayDictionary - { - public static ArrayDictionary ToArrayDictionary(this IEnumerable source, Func keyExtractor) - { - return new ArrayDictionary(source.Select(x => new KeyValuePair(keyExtractor(x), x)).ToArray()); - } - } -} diff --git a/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs b/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs deleted file mode 100644 index 90d547bfa..000000000 --- a/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs +++ /dev/null @@ -1,164 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace Squidex.Infrastructure.Collections -{ - public class ArrayDictionary : IReadOnlyDictionary - { - private readonly IEqualityComparer keyComparer; - private readonly KeyValuePair[] items; - - public TValue this[TKey key] - { - get - { - if (!TryGetValue(key, out var value)) - { - throw new KeyNotFoundException(); - } - - return value; - } - } - - public IEnumerable Keys - { - get { return items.Select(x => x.Key); } - } - - public IEnumerable Values - { - get { return items.Select(x => x.Value); } - } - - public int Count - { - get { return items.Length; } - } - - public ArrayDictionary() - : this(EqualityComparer.Default, Array.Empty>()) - { - } - - public ArrayDictionary(KeyValuePair[] items) - : this(EqualityComparer.Default, items) - { - } - - public ArrayDictionary(IEqualityComparer keyComparer, KeyValuePair[] items) - { - Guard.NotNull(items, nameof(items)); - Guard.NotNull(keyComparer, nameof(keyComparer)); - - this.items = items; - - this.keyComparer = keyComparer; - } - - public KeyValuePair[] With(TKey key, TValue value) - { - var result = new List>(Math.Max(items.Length, 1)); - - var wasReplaced = false; - - for (var i = 0; i < items.Length; i++) - { - var item = items[i]; - - if (wasReplaced || !keyComparer.Equals(item.Key, key)) - { - result.Add(item); - } - else - { - result.Add(new KeyValuePair(key, value)); - wasReplaced = true; - } - } - - if (!wasReplaced) - { - result.Add(new KeyValuePair(key, value)); - } - - return result.ToArray(); - } - - public KeyValuePair[] Without(TKey key) - { - var result = new List>(Math.Max(items.Length, 1)); - - var wasRemoved = false; - - for (var i = 0; i < items.Length; i++) - { - var item = items[i]; - - if (wasRemoved || !keyComparer.Equals(item.Key, key)) - { - result.Add(item); - } - else - { - wasRemoved = true; - } - } - - return result.ToArray(); - } - - public bool ContainsKey(TKey key) - { - for (var i = 0; i < items.Length; i++) - { - if (keyComparer.Equals(items[i].Key, key)) - { - return true; - } - } - - return false; - } - - public bool TryGetValue(TKey key, out TValue value) - { - value = default; - - for (var i = 0; i < items.Length; i++) - { - if (keyComparer.Equals(items[i].Key, key)) - { - value = items[i].Value; - return true; - } - } - - return false; - } - - IEnumerator> IEnumerable>.GetEnumerator() - { - return GetEnumerable(items).GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return items.GetEnumerator(); - } - - private static IEnumerable GetEnumerable(IEnumerable array) - { - return array; - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/CommandContext.cs b/src/Squidex.Infrastructure/Commands/CommandContext.cs deleted file mode 100644 index 0b50bb495..000000000 --- a/src/Squidex.Infrastructure/Commands/CommandContext.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Commands -{ - public sealed class CommandContext - { - private Tuple result; - - public Guid ContextId { get; } = Guid.NewGuid(); - - public ICommand Command { get; } - - public ICommandBus CommandBus { get; } - - public object PlainResult - { - get { return result?.Item1; } - } - - public bool IsCompleted - { - get { return result != null; } - } - - public CommandContext(ICommand command, ICommandBus commandBus) - { - Guard.NotNull(command, nameof(command)); - Guard.NotNull(commandBus, nameof(commandBus)); - - Command = command; - CommandBus = commandBus; - } - - public CommandContext Complete(object resultValue = null) - { - result = Tuple.Create(resultValue); - - return this; - } - - public T Result() - { - return (T)result?.Item1; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs b/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs deleted file mode 100644 index 556f7d1f8..000000000 --- a/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Commands -{ - public sealed class CustomCommandMiddlewareRunner : ICommandMiddleware - { - private readonly IEnumerable extensions; - - public CustomCommandMiddlewareRunner(IEnumerable extensions) - { - Guard.NotNull(extensions, nameof(extensions)); - - this.extensions = extensions.Reverse().ToList(); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - foreach (var handler in extensions) - { - next = Join(handler, context, next); - } - - await next(); - } - - private static Func Join(ICommandMiddleware handler, CommandContext context, Func next) - { - return () => handler.HandleAsync(context, next); - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs deleted file mode 100644 index 21d4a7436..000000000 --- a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.States; - -namespace Squidex.Infrastructure.Commands -{ - public abstract class DomainObjectGrain : DomainObjectGrainBase where T : IDomainState, new() - { - private readonly IStore store; - private T snapshot = new T { Version = EtagVersion.Empty }; - private IPersistence persistence; - - public override T Snapshot - { - get { return snapshot; } - } - - protected DomainObjectGrain(IStore store, ISemanticLog log) - : base(log) - { - Guard.NotNull(store, nameof(store)); - - this.store = store; - } - - protected sealed override void ApplyEvent(Envelope @event) - { - var newVersion = Version + 1; - - snapshot = OnEvent(@event); - snapshot.Version = newVersion; - } - - protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) - { - snapshot = previousSnapshot; - } - - protected sealed override Task ReadAsync(Type type, Guid id) - { - persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, new HandleSnapshot(ApplySnapshot), ApplyEvent); - - return persistence.ReadAsync(); - } - - private void ApplySnapshot(T state) - { - snapshot = state; - } - - protected sealed override async Task WriteAsync(Envelope[] events, long previousVersion) - { - if (events.Length > 0) - { - await persistence.WriteEventsAsync(events); - await persistence.WriteSnapshotAsync(Snapshot); - } - } - - protected T OnEvent(Envelope @event) - { - return Snapshot.Apply(@event); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs deleted file mode 100644 index daf4c21c5..000000000 --- a/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs +++ /dev/null @@ -1,225 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Commands -{ - public abstract class DomainObjectGrainBase : GrainOfGuid, IDomainObjectGrain where T : IDomainState, new() - { - private readonly List> uncomittedEvents = new List>(); - private readonly ISemanticLog log; - private Guid id; - - private enum Mode - { - Create, - Update, - Upsert - } - - public Guid Id - { - get { return id; } - } - - public long Version - { - get { return Snapshot.Version; } - } - - public abstract T Snapshot { get; } - - protected DomainObjectGrainBase(ISemanticLog log) - { - Guard.NotNull(log, nameof(log)); - - this.log = log; - } - - protected override async Task OnActivateAsync(Guid key) - { - var logContext = (key: key.ToString(), name: GetType().Name); - - using (log.MeasureInformation(logContext, (ctx, w) => w - .WriteProperty("action", "ActivateDomainObject") - .WriteProperty("domainObjectType", ctx.name) - .WriteProperty("domainObjectKey", ctx.key))) - { - id = key; - - await ReadAsync(GetType(), id); - } - } - - public void RaiseEvent(IEvent @event) - { - RaiseEvent(Envelope.Create(@event)); - } - - public virtual void RaiseEvent(Envelope @event) - { - Guard.NotNull(@event, nameof(@event)); - - @event.SetAggregateId(id); - - ApplyEvent(@event); - - uncomittedEvents.Add(@event); - } - - public IReadOnlyList> GetUncomittedEvents() - { - return uncomittedEvents; - } - - public void ClearUncommittedEvents() - { - uncomittedEvents.Clear(); - } - - protected Task CreateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler, Mode.Create); - } - - protected Task CreateReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToAsync(), Mode.Create); - } - - protected Task CreateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler.ToDefault(), Mode.Create); - } - - protected Task Create(TCommand command, Action handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault()?.ToAsync(), Mode.Create); - } - - protected Task UpdateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler, Mode.Update); - } - - protected Task UpdateReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToAsync(), Mode.Update); - } - - protected Task UpdateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault(), Mode.Update); - } - - protected Task Update(TCommand command, Action handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault()?.ToAsync(), Mode.Update); - } - - protected Task UpsertReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler, Mode.Upsert); - } - - protected Task UpsertReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToAsync(), Mode.Upsert); - } - - protected Task UpsertAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault(), Mode.Upsert); - } - - protected Task Upsert(TCommand command, Action handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault()?.ToAsync(), Mode.Upsert); - } - - private async Task InvokeAsync(TCommand command, Func> handler, Mode mode) where TCommand : class, IAggregateCommand - { - Guard.NotNull(command, nameof(command)); - - if (command.ExpectedVersion > EtagVersion.Any && command.ExpectedVersion != Version) - { - throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion); - } - - if (mode == Mode.Update && Version < 0) - { - TryDeactivateOnIdle(); - - throw new DomainObjectNotFoundException(id.ToString(), GetType()); - } - - if (mode == Mode.Create && Version >= 0) - { - throw new DomainException("Object has already been created."); - } - - var previousSnapshot = Snapshot; - var previousVersion = Version; - try - { - var result = await handler(command); - - var events = uncomittedEvents.ToArray(); - - await WriteAsync(events, previousVersion); - - if (result == null) - { - if (mode == Mode.Update || (mode == Mode.Upsert && Version == 0)) - { - result = new EntitySavedResult(Version); - } - else - { - result = EntityCreatedResult.Create(id, Version); - } - } - - return result; - } - catch - { - RestorePreviousSnapshot(previousSnapshot, previousVersion); - - throw; - } - finally - { - ClearUncommittedEvents(); - } - } - - protected abstract void RestorePreviousSnapshot(T previousSnapshot, long previousVersion); - - protected abstract void ApplyEvent(Envelope @event); - - protected abstract Task ReadAsync(Type type, Guid id); - - protected abstract Task WriteAsync(Envelope[] events, long previousVersion); - - public async Task> ExecuteAsync(J command) - { - var result = await ExecuteAsync(command.Value); - - return result; - } - - protected abstract Task ExecuteAsync(IAggregateCommand command); - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs deleted file mode 100644 index 68434279d..000000000 --- a/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Orleans; - -namespace Squidex.Infrastructure.Commands -{ - public static class DomainObjectGrainFormatter - { - public static string Format(IGrainCallContext context) - { - if (context.InterfaceMethod == null) - { - return "Unknown"; - } - - if (string.Equals(context.InterfaceMethod.Name, nameof(IDomainObjectGrain.ExecuteAsync), StringComparison.CurrentCultureIgnoreCase) && - context.Arguments?.Length == 1 && - context.Arguments[0] != null) - { - var argumentFullName = context.Arguments[0].ToString(); - var argumentParts = argumentFullName.Split('.'); - var argumentName = argumentParts[argumentParts.Length - 1]; - - return $"{nameof(IDomainObjectGrain.ExecuteAsync)}({argumentName})"; - } - - return context.InterfaceMethod.Name; - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs b/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs deleted file mode 100644 index ba2d8b56c..000000000 --- a/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using NodaTime; - -namespace Squidex.Infrastructure.Commands -{ - public sealed class EnrichWithTimestampCommandMiddleware : ICommandMiddleware - { - private readonly IClock clock; - - public EnrichWithTimestampCommandMiddleware(IClock clock) - { - Guard.NotNull(clock, nameof(clock)); - - this.clock = clock; - } - - public Task HandleAsync(CommandContext context, Func next) - { - if (context.Command is ITimestampCommand timestampCommand) - { - timestampCommand.Timestamp = clock.GetCurrentInstant(); - } - - return next(); - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs b/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs deleted file mode 100644 index 9dfb70576..000000000 --- a/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; - -namespace Squidex.Infrastructure.Commands -{ - public class GrainCommandMiddleware : ICommandMiddleware where TCommand : IAggregateCommand where TGrain : IDomainObjectGrain - { - private readonly IGrainFactory grainFactory; - - public GrainCommandMiddleware(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public virtual async Task HandleAsync(CommandContext context, Func next) - { - await ExecuteCommandAsync(context); - - await next(); - } - - protected async Task ExecuteCommandAsync(CommandContext context) - { - if (context.Command is TCommand typedCommand) - { - var result = await ExecuteCommandAsync(typedCommand); - - context.Complete(result); - } - } - - private async Task ExecuteCommandAsync(TCommand typedCommand) - { - var grain = grainFactory.GetGrain(typedCommand.AggregateId); - - var result = await grain.ExecuteAsync(typedCommand); - - return result.Value; - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs deleted file mode 100644 index f52ce2122..000000000 --- a/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Infrastructure.Commands -{ - public interface IDomainObjectGrain : IGrainWithGuidKey - { - Task> ExecuteAsync(J command); - } -} diff --git a/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs b/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs deleted file mode 100644 index 7c72e8fba..000000000 --- a/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Commands -{ - public sealed class InMemoryCommandBus : ICommandBus - { - private readonly List middlewares; - - public InMemoryCommandBus(IEnumerable middlewares) - { - Guard.NotNull(middlewares, nameof(middlewares)); - - this.middlewares = middlewares.Reverse().ToList(); - } - - public async Task PublishAsync(ICommand command) - { - Guard.NotNull(command, nameof(command)); - - var context = new CommandContext(command, this); - - var next = new Func(() => TaskHelper.Done); - - foreach (var handler in middlewares) - { - next = Join(handler, context, next); - } - - await next(); - - return context; - } - - private static Func Join(ICommandMiddleware handler, CommandContext context, Func next) - { - return () => handler.HandleAsync(context, next); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs b/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs deleted file mode 100644 index 0e0d64504..000000000 --- a/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.Commands -{ - public sealed class LogCommandMiddleware : ICommandMiddleware - { - private readonly ISemanticLog log; - - public LogCommandMiddleware(ISemanticLog log) - { - Guard.NotNull(log, nameof(log)); - - this.log = log; - } - - public async Task HandleAsync(CommandContext context, Func next) - { - var logContext = (id: context.ContextId.ToString(), command: context.Command.GetType().Name); - - try - { - log.LogInformation(logContext, (ctx, w) => w - .WriteProperty("action", "HandleCommand.") - .WriteProperty("actionId", ctx.id) - .WriteProperty("status", "Started") - .WriteProperty("commandType", ctx.command)); - - using (log.MeasureInformation(logContext, (ctx, w) => w - .WriteProperty("action", "HandleCommand.") - .WriteProperty("actionId", ctx.id) - .WriteProperty("status", "Completed") - .WriteProperty("commandType", ctx.command))) - { - await next(); - } - - log.LogInformation(logContext, (ctx, w) => w - .WriteProperty("action", "HandleCommand.") - .WriteProperty("actionId", ctx.id) - .WriteProperty("status", "Succeeded") - .WriteProperty("commandType", ctx.command)); - } - catch (Exception ex) - { - log.LogError(ex, logContext, (ctx, w) => w - .WriteProperty("action", "HandleCommand.") - .WriteProperty("actionId", ctx.id) - .WriteProperty("status", "Failed") - .WriteProperty("commandType", ctx.command)); - - throw; - } - - if (!context.IsCompleted) - { - log.LogFatal(logContext, (ctx, w) => w - .WriteProperty("action", "HandleCommand.") - .WriteProperty("actionId", ctx.id) - .WriteProperty("status", "Unhandled") - .WriteProperty("commandType", ctx.command)); - } - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs deleted file mode 100644 index 6ff25efc6..000000000 --- a/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.States; - -namespace Squidex.Infrastructure.Commands -{ - public abstract class LogSnapshotDomainObjectGrain : DomainObjectGrainBase where T : IDomainState, new() - { - private readonly IStore store; - private readonly List snapshots = new List { new T { Version = EtagVersion.Empty } }; - private IPersistence persistence; - - public override T Snapshot - { - get { return snapshots.Last(); } - } - - protected LogSnapshotDomainObjectGrain(IStore store, ISemanticLog log) - : base(log) - { - Guard.NotNull(log, nameof(log)); - - this.store = store; - } - - public T GetSnapshot(long version) - { - if (version == EtagVersion.Any || version == EtagVersion.Auto) - { - return Snapshot; - } - - if (version == EtagVersion.Empty) - { - return snapshots[0]; - } - - if (version >= 0 && version < snapshots.Count - 1) - { - return snapshots[(int)version + 1]; - } - - return default; - } - - protected sealed override void ApplyEvent(Envelope @event) - { - var snapshot = OnEvent(@event); - - snapshot.Version = Version + 1; - snapshots.Add(snapshot); - } - - protected sealed override Task ReadAsync(Type type, Guid id) - { - persistence = store.WithEventSourcing(type, id, ApplyEvent); - - return persistence.ReadAsync(); - } - - protected sealed override async Task WriteAsync(Envelope[] events, long previousVersion) - { - if (events.Length > 0) - { - var persistedSnapshots = store.GetSnapshotStore(); - - await persistence.WriteEventsAsync(events); - await persistedSnapshots.WriteAsync(Id, Snapshot, previousVersion, previousVersion + events.Length); - } - } - - protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) - { - while (snapshots.Count > previousVersion + 2) - { - snapshots.RemoveAt(snapshots.Count - 1); - } - } - - protected T OnEvent(Envelope @event) - { - return Snapshot.Apply(@event); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs b/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs deleted file mode 100644 index c4c5ed3e9..000000000 --- a/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; - -namespace Squidex.Infrastructure.Commands -{ - public sealed class ReadonlyCommandMiddleware : ICommandMiddleware - { - private readonly ReadonlyOptions options; - - public ReadonlyCommandMiddleware(IOptions options) - { - Guard.NotNull(options, nameof(options)); - - this.options = options.Value; - } - - public Task HandleAsync(CommandContext context, Func next) - { - if (options.IsReadonly) - { - throw new DomainException("Application is in readonly mode at the moment."); - } - - return next(); - } - } -} diff --git a/src/Squidex.Infrastructure/DelegateDisposable.cs b/src/Squidex.Infrastructure/DelegateDisposable.cs deleted file mode 100644 index bbdbb0262..000000000 --- a/src/Squidex.Infrastructure/DelegateDisposable.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure -{ - public sealed class DelegateDisposable : IDisposable - { - private readonly Action action; - - public DelegateDisposable(Action action) - { - Guard.NotNull(action, nameof(action)); - - this.action = action; - } - - public void Dispose() - { - action(); - } - } -} diff --git a/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs b/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs deleted file mode 100644 index 3a7ebfb76..000000000 --- a/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Squidex.Infrastructure; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class DependencyInjectionExtensions - { - public delegate void Registrator(Type serviceType, Func implementationFactory); - - public sealed class InterfaceRegistrator - { - private readonly Registrator register; - private readonly Registrator registerOptional; - - public InterfaceRegistrator(Registrator register, Registrator registerOptional) - { - this.register = register; - this.registerOptional = registerOptional; - - var interfaces = typeof(T).GetInterfaces(); - - if (interfaces.Contains(typeof(IInitializable))) - { - register(typeof(IInitializable), c => c.GetRequiredService()); - } - - if (interfaces.Contains(typeof(IBackgroundProcess))) - { - register(typeof(IBackgroundProcess), c => c.GetRequiredService()); - } - } - - public InterfaceRegistrator AsSelf() - { - return this; - } - - public InterfaceRegistrator AsOptional() - { - if (typeof(TInterface) != typeof(T)) - { - registerOptional(typeof(TInterface), c => c.GetRequiredService()); - } - - return this; - } - - public InterfaceRegistrator As() - { - if (typeof(TInterface) != typeof(T)) - { - register(typeof(TInterface), c => c.GetRequiredService()); - } - - return this; - } - } - - public static InterfaceRegistrator AddTransientAs(this IServiceCollection services, Func factory) where T : class - { - services.AddTransient(typeof(T), factory); - - return new InterfaceRegistrator((t, f) => services.AddTransient(t, f), services.TryAddTransient); - } - - public static InterfaceRegistrator AddTransientAs(this IServiceCollection services) where T : class - { - services.AddTransient(); - - return new InterfaceRegistrator((t, f) => services.AddTransient(t, f), services.TryAddTransient); - } - - public static InterfaceRegistrator AddSingletonAs(this IServiceCollection services, Func factory) where T : class - { - services.AddSingleton(typeof(T), factory); - - return new InterfaceRegistrator((t, f) => services.AddSingleton(t, f), services.TryAddSingleton); - } - - public static InterfaceRegistrator AddSingletonAs(this IServiceCollection services) where T : class - { - services.AddSingleton(); - - return new InterfaceRegistrator((t, f) => services.AddSingleton(t, f), services.TryAddSingleton); - } - } -} diff --git a/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs b/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs deleted file mode 100644 index 7baf5921f..000000000 --- a/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Options; - -namespace Squidex.Infrastructure.Diagnostics -{ - public sealed class GCHealthCheck : IHealthCheck - { - private readonly long threshold; - - public GCHealthCheck(IOptions options) - { - Guard.NotNull(options, nameof(options)); - - threshold = 1024 * 1024 * options.Value.Threshold; - } - - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - var allocated = GC.GetTotalMemory(false); - - var data = new Dictionary - { - { "Allocated", allocated.ToReadableSize() }, - { "Gen0Collections", GC.CollectionCount(0) }, - { "Gen1Collections", GC.CollectionCount(1) }, - { "Gen2Collections", GC.CollectionCount(2) } - }; - - var status = allocated < threshold ? HealthStatus.Healthy : HealthStatus.Unhealthy; - - return Task.FromResult(new HealthCheckResult(status, $"Application must consum less than {threshold.ToReadableSize()} memory.", data: data)); - } - } -} diff --git a/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs b/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs deleted file mode 100644 index 7d2495ae9..000000000 --- a/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Orleans; -using Orleans.Runtime; - -namespace Squidex.Infrastructure.Diagnostics -{ - public sealed class OrleansHealthCheck : IHealthCheck - { - private readonly IManagementGrain managementGrain; - - public OrleansHealthCheck(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - managementGrain = grainFactory.GetGrain(0); - } - - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - var activationCount = await managementGrain.GetTotalActivationCount(); - - var status = activationCount > 0 ? HealthStatus.Healthy : HealthStatus.Unhealthy; - - return new HealthCheckResult(status, "Orleans must have at least one activation."); - } - } -} diff --git a/src/Squidex.Infrastructure/DomainException.cs b/src/Squidex.Infrastructure/DomainException.cs deleted file mode 100644 index 9adfb4053..000000000 --- a/src/Squidex.Infrastructure/DomainException.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.Serialization; - -namespace Squidex.Infrastructure -{ - [Serializable] - public class DomainException : Exception - { - public DomainException(string message) - : base(message) - { - } - - public DomainException(string message, Exception inner) - : base(message, inner) - { - } - - protected DomainException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} diff --git a/src/Squidex.Infrastructure/DomainObjectException.cs b/src/Squidex.Infrastructure/DomainObjectException.cs deleted file mode 100644 index 24d311688..000000000 --- a/src/Squidex.Infrastructure/DomainObjectException.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.Serialization; - -namespace Squidex.Infrastructure -{ - [Serializable] - public class DomainObjectException : Exception - { - private readonly string id; - private readonly string typeName; - - public string TypeName - { - get { return typeName; } - } - - public string Id - { - get { return id; } - } - - protected DomainObjectException(string message, string id, Type type, Exception inner = null) - : base(message, inner) - { - this.id = id; - - typeName = type?.Name; - } - - protected DomainObjectException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - id = info.GetString(nameof(id)); - - typeName = info.GetString(nameof(typeName)); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(id), id); - info.AddValue(nameof(typeName), typeName); - - base.GetObjectData(info, context); - } - } -} diff --git a/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs b/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs deleted file mode 100644 index 1c73283f3..000000000 --- a/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Net; -using System.Net.Mail; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; - -namespace Squidex.Infrastructure.Email -{ - public sealed class SmtpEmailSender : IEmailSender - { - private readonly SmtpClient smtpClient; - private readonly string sender; - - public SmtpEmailSender(IOptions options) - { - Guard.NotNull(options, nameof(options)); - - var config = options.Value; - - smtpClient = new SmtpClient(config.Server, config.Port) - { - Credentials = new NetworkCredential( - config.Username, - config.Password), - EnableSsl = config.EnableSsl - }; - - sender = config.Sender; - } - - public Task SendAsync(string recipient, string subject, string body) - { - return smtpClient.SendMailAsync(sender, recipient, subject, body); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/CompoundEventConsumer.cs b/src/Squidex.Infrastructure/EventSourcing/CompoundEventConsumer.cs deleted file mode 100644 index 8e93b73f8..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/CompoundEventConsumer.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class CompoundEventConsumer : IEventConsumer - { - private readonly IEventConsumer[] inners; - - public string Name { get; } - - public string EventsFilter { get; } - - public CompoundEventConsumer(IEventConsumer first, params IEventConsumer[] inners) - : this(first?.Name, first, inners) - { - } - - public CompoundEventConsumer(IEventConsumer[] inners) - { - Guard.NotNull(inners, nameof(inners)); - Guard.NotEmpty(inners, nameof(inners)); - - this.inners = inners; - - Name = inners.First().Name; - - var innerFilters = - this.inners.Where(x => !string.IsNullOrWhiteSpace(x.EventsFilter)) - .Select(x => $"({x.EventsFilter})"); - - EventsFilter = string.Join("|", innerFilters); - } - - public CompoundEventConsumer(string name, IEventConsumer first, params IEventConsumer[] inners) - { - Guard.NotNull(first, nameof(first)); - Guard.NotNull(inners, nameof(inners)); - Guard.NotNullOrEmpty(name, nameof(name)); - - this.inners = new[] { first }.Union(inners).ToArray(); - - Name = name; - - var innerFilters = - this.inners.Where(x => !string.IsNullOrWhiteSpace(x.EventsFilter)) - .Select(x => $"({x.EventsFilter})"); - - EventsFilter = string.Join("|", innerFilters); - } - - public bool Handles(StoredEvent @event) - { - return inners.Any(x => x.Handles(@event)); - } - - public Task ClearAsync() - { - return Task.WhenAll(inners.Select(i => i.ClearAsync())); - } - - public async Task On(Envelope @event) - { - foreach (var inner in inners) - { - await inner.On(@event); - } - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs b/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs deleted file mode 100644 index 76ec225dd..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Diagnostics; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Migrations; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class DefaultEventDataFormatter : IEventDataFormatter - { - private readonly IJsonSerializer serializer; - private readonly TypeNameRegistry typeNameRegistry; - - public DefaultEventDataFormatter(TypeNameRegistry typeNameRegistry, IJsonSerializer serializer) - { - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); - Guard.NotNull(serializer, nameof(serializer)); - - this.typeNameRegistry = typeNameRegistry; - - this.serializer = serializer; - } - - public Envelope Parse(EventData eventData, Func stringConverter = null) - { - var payloadType = typeNameRegistry.GetType(eventData.Type); - var payloadObj = serializer.Deserialize(eventData.Payload, payloadType, stringConverter); - - if (payloadObj is IMigrated migratedEvent) - { - payloadObj = migratedEvent.Migrate(); - - if (ReferenceEquals(migratedEvent, payloadObj)) - { - Debug.WriteLine("Migration should return new event."); - } - } - - var envelope = new Envelope(payloadObj, eventData.Headers); - - return envelope; - } - - public EventData ToEventData(Envelope envelope, Guid commitId, bool migrate = true) - { - var eventPayload = envelope.Payload; - - if (migrate && eventPayload is IMigrated migratedEvent) - { - eventPayload = migratedEvent.Migrate(); - - if (ReferenceEquals(migratedEvent, eventPayload)) - { - Debug.WriteLine("Migration should return new event."); - } - } - - var payloadType = typeNameRegistry.GetName(eventPayload.GetType()); - var payloadJson = serializer.Serialize(envelope.Payload); - - envelope.SetCommitId(commitId); - - return new EventData(payloadType, envelope.Headers, payloadJson); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs b/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs deleted file mode 100644 index ba1b59a34..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.EventSourcing -{ - public class Envelope where T : class, IEvent - { - private readonly EnvelopeHeaders headers; - private readonly T payload; - - public EnvelopeHeaders Headers - { - get { return headers; } - } - - public T Payload - { - get { return payload; } - } - - public Envelope(T payload, EnvelopeHeaders headers = null) - { - Guard.NotNull(payload, nameof(payload)); - - this.payload = payload; - this.headers = headers ?? new EnvelopeHeaders(); - } - - public Envelope To() where TOther : class, IEvent - { - return new Envelope(payload as TOther, headers.Clone()); - } - - public static implicit operator Envelope(Envelope source) - { - return source == null ? source : new Envelope(source.payload, source.headers); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/EventData.cs b/src/Squidex.Infrastructure/EventSourcing/EventData.cs deleted file mode 100644 index 016043919..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/EventData.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class EventData - { - public EnvelopeHeaders Headers { get; } - - public string Payload { get; } - - public string Type { get; set; } - - public EventData(string type, EnvelopeHeaders headers, string payload) - { - Guard.NotNull(type, nameof(type)); - Guard.NotNull(headers, nameof(headers)); - Guard.NotNull(payload, nameof(payload)); - - Headers = headers; - - Payload = payload; - - Type = type; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs deleted file mode 100644 index edfca3ddf..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs +++ /dev/null @@ -1,305 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Orleans; -using Orleans.Concurrency; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public class EventConsumerGrain : GrainOfString, IEventConsumerGrain - { - private readonly EventConsumerFactory eventConsumerFactory; - private readonly IGrainState state; - private readonly IEventDataFormatter eventDataFormatter; - private readonly IEventStore eventStore; - private readonly ISemanticLog log; - private TaskScheduler scheduler; - private IEventSubscription currentSubscription; - private IEventConsumer eventConsumer; - - public EventConsumerGrain( - EventConsumerFactory eventConsumerFactory, - IGrainState state, - IEventStore eventStore, - IEventDataFormatter eventDataFormatter, - ISemanticLog log) - { - Guard.NotNull(eventStore, nameof(eventStore)); - Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); - Guard.NotNull(eventConsumerFactory, nameof(eventConsumerFactory)); - Guard.NotNull(state, nameof(state)); - Guard.NotNull(log, nameof(log)); - - this.eventStore = eventStore; - this.eventDataFormatter = eventDataFormatter; - this.eventConsumerFactory = eventConsumerFactory; - this.state = state; - - this.log = log; - } - - protected override Task OnActivateAsync(string key) - { - scheduler = TaskScheduler.Current; - - eventConsumer = eventConsumerFactory(key); - - return TaskHelper.Done; - } - - public Task> GetStateAsync() - { - return Task.FromResult(CreateInfo()); - } - - private Immutable CreateInfo() - { - return state.Value.ToInfo(eventConsumer.Name).AsImmutable(); - } - - public Task OnEventAsync(Immutable subscription, Immutable storedEvent) - { - if (subscription.Value != currentSubscription) - { - return TaskHelper.Done; - } - - return DoAndUpdateStateAsync(async () => - { - if (eventConsumer.Handles(storedEvent.Value)) - { - var @event = ParseKnownEvent(storedEvent.Value); - - if (@event != null) - { - await DispatchConsumerAsync(@event); - } - } - - state.Value = state.Value.Handled(storedEvent.Value.EventPosition); - }); - } - - public Task OnErrorAsync(Immutable subscription, Immutable exception) - { - if (subscription.Value != currentSubscription) - { - return TaskHelper.Done; - } - - return DoAndUpdateStateAsync(() => - { - Unsubscribe(); - - state.Value = state.Value.Failed(exception.Value); - }); - } - - public Task ActivateAsync() - { - if (!state.Value.IsStopped) - { - Subscribe(state.Value.Position); - } - - return TaskHelper.Done; - } - - public async Task> StartAsync() - { - if (!state.Value.IsStopped) - { - return CreateInfo(); - } - - await DoAndUpdateStateAsync(() => - { - Subscribe(state.Value.Position); - - state.Value = state.Value.Started(); - }); - - return CreateInfo(); - } - - public async Task> StopAsync() - { - if (state.Value.IsStopped) - { - return CreateInfo(); - } - - await DoAndUpdateStateAsync(() => - { - Unsubscribe(); - - state.Value = state.Value.Stopped(); - }); - - return CreateInfo(); - } - - public async Task> ResetAsync() - { - await DoAndUpdateStateAsync(async () => - { - Unsubscribe(); - - await ClearAsync(); - - Subscribe(null); - - state.Value = state.Value.Reset(); - }); - - return CreateInfo(); - } - - private Task DoAndUpdateStateAsync(Action action, [CallerMemberName] string caller = null) - { - return DoAndUpdateStateAsync(() => { action(); return TaskHelper.Done; }, caller); - } - - private async Task DoAndUpdateStateAsync(Func action, [CallerMemberName] string caller = null) - { - try - { - await action(); - } - catch (Exception ex) - { - try - { - Unsubscribe(); - } - catch (Exception unsubscribeException) - { - ex = new AggregateException(ex, unsubscribeException); - } - - log.LogFatal(ex, w => w - .WriteProperty("action", caller) - .WriteProperty("status", "Failed") - .WriteProperty("eventConsumer", eventConsumer.Name)); - - state.Value = state.Value.Failed(ex); - } - - await state.WriteAsync(); - } - - private async Task ClearAsync() - { - var logContext = (actionId: Guid.NewGuid().ToString(), consumer: eventConsumer.Name); - - log.LogInformation(logContext, (ctx, w) => w - .WriteProperty("action", "EventConsumerReset") - .WriteProperty("actionId", ctx.actionId) - .WriteProperty("status", "Started") - .WriteProperty("eventConsumer", ctx.consumer)); - - using (log.MeasureTrace(logContext, (ctx, w) => w - .WriteProperty("action", "EventConsumerReset") - .WriteProperty("actionId", ctx.actionId) - .WriteProperty("status", "Completed") - .WriteProperty("eventConsumer", ctx.consumer))) - { - await eventConsumer.ClearAsync(); - } - } - - private async Task DispatchConsumerAsync(Envelope @event) - { - var eventId = @event.Headers.EventId().ToString(); - var eventType = @event.Payload.GetType().Name; - - var logContext = (eventId, eventType, consumer: eventConsumer.Name); - - log.LogInformation(logContext, (ctx, w) => w - .WriteProperty("action", "HandleEvent") - .WriteProperty("actionId", ctx.eventId) - .WriteProperty("status", "Started") - .WriteProperty("eventId", ctx.eventId) - .WriteProperty("eventType", ctx.eventType) - .WriteProperty("eventConsumer", ctx.consumer)); - - using (log.MeasureTrace(logContext, (ctx, w) => w - .WriteProperty("action", "HandleEvent") - .WriteProperty("actionId", ctx.eventId) - .WriteProperty("status", "Completed") - .WriteProperty("eventId", ctx.eventId) - .WriteProperty("eventType", ctx.eventType) - .WriteProperty("eventConsumer", ctx.consumer))) - { - await eventConsumer.On(@event); - } - } - - private void Unsubscribe() - { - if (currentSubscription != null) - { - currentSubscription.StopAsync().Forget(); - currentSubscription = null; - } - } - - private void Subscribe(string position) - { - if (currentSubscription == null) - { - currentSubscription?.StopAsync().Forget(); - currentSubscription = CreateSubscription(eventConsumer.EventsFilter, position); - } - else - { - currentSubscription.WakeUp(); - } - } - - private Envelope ParseKnownEvent(StoredEvent message) - { - try - { - var @event = eventDataFormatter.Parse(message.Data); - - @event.SetEventPosition(message.EventPosition); - @event.SetEventStreamNumber(message.EventStreamNumber); - - return @event; - } - catch (TypeNameNotFoundException) - { - log.LogDebug(w => w.WriteProperty("oldEventFound", message.Data.Type)); - - return null; - } - } - - protected virtual IEventConsumerGrain GetSelf() - { - return this.AsReference(); - } - - protected virtual IEventSubscription CreateSubscription(IEventStore store, IEventSubscriber subscriber, string streamFilter, string position) - { - return new RetrySubscription(store, subscriber, streamFilter, position); - } - - private IEventSubscription CreateSubscription(string streamFilter, string position) - { - return CreateSubscription(eventStore, new WrapperSubscription(GetSelf(), scheduler), streamFilter, position); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs deleted file mode 100644 index 4952088c0..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Orleans; -using Orleans.Concurrency; -using Orleans.Core; -using Orleans.Runtime; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public class EventConsumerManagerGrain : Grain, IEventConsumerManagerGrain, IRemindable - { - private readonly IEnumerable eventConsumers; - - public EventConsumerManagerGrain(IEnumerable eventConsumers) - : this(eventConsumers, null, null) - { - } - - protected EventConsumerManagerGrain( - IEnumerable eventConsumers, - IGrainIdentity identity, - IGrainRuntime runtime) - : base(identity, runtime) - { - Guard.NotNull(eventConsumers, nameof(eventConsumers)); - - this.eventConsumers = eventConsumers; - } - - public override Task OnActivateAsync() - { - DelayDeactivation(TimeSpan.FromDays(1)); - - RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); - RegisterTimer(x => ActivateAsync(null), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); - - return Task.FromResult(true); - } - - public Task ActivateAsync(string streamName) - { - var tasks = - eventConsumers - .Where(c => streamName == null || Regex.IsMatch(streamName, c.EventsFilter)) - .Select(c => GrainFactory.GetGrain(c.Name)) - .Select(c => c.ActivateAsync()); - - return Task.WhenAll(tasks); - } - - public async Task>> GetConsumersAsync() - { - var tasks = - eventConsumers - .Select(c => GrainFactory.GetGrain(c.Name)) - .Select(c => c.GetStateAsync()); - - var consumerInfos = await Task.WhenAll(tasks); - - return new Immutable>(consumerInfos.Select(r => r.Value).ToList()); - } - - public Task StartAllAsync() - { - return Task.WhenAll( - eventConsumers - .Select(c => StartAsync(c.Name))); - } - - public Task StopAllAsync() - { - return Task.WhenAll( - eventConsumers - .Select(c => StopAsync(c.Name))); - } - - public Task> ResetAsync(string consumerName) - { - var eventConsumer = GrainFactory.GetGrain(consumerName); - - return eventConsumer.ResetAsync(); - } - - public Task> StartAsync(string consumerName) - { - var eventConsumer = GrainFactory.GetGrain(consumerName); - - return eventConsumer.StartAsync(); - } - - public Task> StopAsync(string consumerName) - { - var eventConsumer = GrainFactory.GetGrain(consumerName); - - return eventConsumer.StopAsync(); - } - - public Task ActivateAsync() - { - return ActivateAsync(null); - } - - public Task ReceiveReminder(string reminderName, TickStatus status) - { - return ActivateAsync(null); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs deleted file mode 100644 index 2b0ec771e..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public sealed class EventConsumerState - { - public bool IsStopped { get; set; } - - public string Error { get; set; } - - public string Position { get; set; } - - public EventConsumerState Reset() - { - return new EventConsumerState(); - } - - public EventConsumerState Handled(string position) - { - return new EventConsumerState { Position = position }; - } - - public EventConsumerState Failed(Exception ex) - { - return new EventConsumerState { Position = Position, IsStopped = true, Error = ex?.ToString() }; - } - - public EventConsumerState Stopped() - { - return new EventConsumerState { Position = Position, IsStopped = true }; - } - - public EventConsumerState Started() - { - return new EventConsumerState { Position = Position, IsStopped = false }; - } - - public EventConsumerInfo ToInfo(string name) - { - return SimpleMapper.Map(this, new EventConsumerInfo { Name = name }); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs deleted file mode 100644 index 6e3da7063..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Orleans; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public sealed class OrleansEventNotifier : IEventNotifier - { - private readonly Lazy eventConsumerManagerGrain; - - public OrleansEventNotifier(IGrainFactory factory) - { - Guard.NotNull(factory, nameof(factory)); - - eventConsumerManagerGrain = new Lazy(() => - { - return factory.GetGrain(SingleGrain.Id); - }); - } - - public void NotifyEventsStored(string streamName) - { - eventConsumerManagerGrain.Value.ActivateAsync(streamName); - } - - public IDisposable Subscribe(Action handler) - { - return null; - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventDataFormatter.cs b/src/Squidex.Infrastructure/EventSourcing/IEventDataFormatter.cs deleted file mode 100644 index db3c3fdff..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/IEventDataFormatter.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.EventSourcing -{ - public interface IEventDataFormatter - { - Envelope Parse(EventData eventData, Func stringConverter = null); - - EventData ToEventData(Envelope envelope, Guid commitId, bool migrate = true); - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs b/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs deleted file mode 100644 index 6ee608632..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.EventSourcing -{ - public interface IEventStore - { - Task CreateIndexAsync(string property); - - Task> QueryAsync(string streamName, long streamPosition = 0); - - Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default); - - Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default); - - Task AppendAsync(Guid commitId, string streamName, ICollection events); - - Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events); - - Task DeleteStreamAsync(string streamName); - - IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null); - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs b/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs deleted file mode 100644 index 59b8047cb..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.Timers; - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class PollingSubscription : IEventSubscription - { - private readonly CompletionTimer timer; - - public PollingSubscription( - IEventStore eventStore, - IEventSubscriber eventSubscriber, - string streamFilter, - string position) - { - Guard.NotNull(eventStore, nameof(eventStore)); - Guard.NotNull(eventSubscriber, nameof(eventSubscriber)); - - timer = new CompletionTimer(5000, async ct => - { - try - { - await eventStore.QueryAsync(async storedEvent => - { - await eventSubscriber.OnEventAsync(this, storedEvent); - - position = storedEvent.EventPosition; - }, streamFilter, position, ct); - } - catch (Exception ex) - { - if (!ex.Is()) - { - await eventSubscriber.OnErrorAsync(this, ex); - } - } - }); - } - - public void WakeUp() - { - timer.SkipCurrentDelay(); - } - - public Task StopAsync() - { - return timer.StopAsync(); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs b/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs deleted file mode 100644 index c995704ef..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs +++ /dev/null @@ -1,117 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -#pragma warning disable RECS0002 // Convert anonymous method to method group - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class RetrySubscription : IEventSubscription, IEventSubscriber - { - private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(10); - private readonly CancellationTokenSource timerCts = new CancellationTokenSource(); - private readonly RetryWindow retryWindow = new RetryWindow(TimeSpan.FromMinutes(5), 5); - private readonly IEventStore eventStore; - private readonly IEventSubscriber eventSubscriber; - private readonly string streamFilter; - private IEventSubscription currentSubscription; - private string position; - - public int ReconnectWaitMs { get; set; } = 5000; - - public RetrySubscription(IEventStore eventStore, IEventSubscriber eventSubscriber, string streamFilter, string position) - { - Guard.NotNull(eventStore, nameof(eventStore)); - Guard.NotNull(eventSubscriber, nameof(eventSubscriber)); - Guard.NotNull(streamFilter, nameof(streamFilter)); - - this.position = position; - - this.eventStore = eventStore; - this.eventSubscriber = eventSubscriber; - - this.streamFilter = streamFilter; - - Subscribe(); - } - - private void Subscribe() - { - if (currentSubscription == null) - { - currentSubscription = eventStore.CreateSubscription(this, streamFilter, position); - } - } - - private void Unsubscribe() - { - currentSubscription?.StopAsync().Forget(); - currentSubscription = null; - } - - public void WakeUp() - { - currentSubscription?.WakeUp(); - } - - private async Task HandleEventAsync(IEventSubscription subscription, StoredEvent storedEvent) - { - if (subscription == currentSubscription) - { - await eventSubscriber.OnEventAsync(this, storedEvent); - - position = storedEvent.EventPosition; - } - } - - private async Task HandleErrorAsync(IEventSubscription subscription, Exception exception) - { - if (subscription == currentSubscription) - { - Unsubscribe(); - - if (retryWindow.CanRetryAfterFailure()) - { - RetryAsync().Forget(); - } - else - { - await eventSubscriber.OnErrorAsync(this, exception); - } - } - } - - private async Task RetryAsync() - { - await Task.Delay(ReconnectWaitMs, timerCts.Token); - - await dispatcher.DispatchAsync(Subscribe); - } - - Task IEventSubscriber.OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) - { - return dispatcher.DispatchAsync(() => HandleEventAsync(subscription, storedEvent)); - } - - Task IEventSubscriber.OnErrorAsync(IEventSubscription subscription, Exception exception) - { - return dispatcher.DispatchAsync(() => HandleErrorAsync(subscription, exception)); - } - - public async Task StopAsync() - { - await dispatcher.DispatchAsync(Unsubscribe); - await dispatcher.StopAndWaitAsync(); - - timerCts.Cancel(); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs b/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs deleted file mode 100644 index 97ee0f55c..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class StoredEvent - { - public string StreamName { get; } - - public string EventPosition { get; } - - public long EventStreamNumber { get; } - - public EventData Data { get; } - - public StoredEvent(string streamName, string eventPosition, long eventStreamNumber, EventData data) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNullOrEmpty(eventPosition, nameof(eventPosition)); - Guard.NotNull(data, nameof(data)); - - Data = data; - - EventPosition = eventPosition; - EventStreamNumber = eventStreamNumber; - - StreamName = streamName; - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs b/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs deleted file mode 100644 index b3bc063af..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.EventSourcing -{ - public static class StreamFilter - { - public static bool IsAll(string filter) - { - return string.IsNullOrWhiteSpace(filter) - || string.Equals(filter, ".*", StringComparison.OrdinalIgnoreCase) - || string.Equals(filter, "(.*)", StringComparison.OrdinalIgnoreCase) - || string.Equals(filter, "(.*?)", StringComparison.OrdinalIgnoreCase); - } - } -} diff --git a/src/Squidex.Infrastructure/Guard.cs b/src/Squidex.Infrastructure/Guard.cs deleted file mode 100644 index c29b11827..000000000 --- a/src/Squidex.Infrastructure/Guard.cs +++ /dev/null @@ -1,219 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Infrastructure -{ - public static class Guard - { - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidNumber(float target, string parameterName) - { - if (float.IsNaN(target) || float.IsPositiveInfinity(target) || float.IsNegativeInfinity(target)) - { - throw new ArgumentException("Value must be a valid number.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidNumber(double target, string parameterName) - { - if (double.IsNaN(target) || double.IsPositiveInfinity(target) || double.IsNegativeInfinity(target)) - { - throw new ArgumentException("Value must be a valid number.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidSlug(string target, string parameterName) - { - NotNullOrEmpty(target, parameterName); - - if (!target.IsSlug()) - { - throw new ArgumentException("Target is not a valid slug.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidPropertyName(string target, string parameterName) - { - NotNullOrEmpty(target, parameterName); - - if (!target.IsPropertyName()) - { - throw new ArgumentException("Target is not a valid property name.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void HasType(object target, string parameterName) - { - if (target != null && target.GetType() != typeof(T)) - { - throw new ArgumentException($"The parameter must be of type {typeof(T)}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void HasType(object target, Type expectedType, string parameterName) - { - if (target != null && expectedType != null && target.GetType() != expectedType) - { - throw new ArgumentException($"The parameter must be of type {expectedType}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Between(TValue target, TValue lower, TValue upper, string parameterName) where TValue : IComparable - { - if (!target.IsBetween(lower, upper)) - { - throw new ArgumentException($"Value must be between {lower} and {upper}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Enum(TEnum target, string parameterName) where TEnum : struct - { - if (!target.IsEnumValue()) - { - throw new ArgumentException($"Value must be a valid enum type {typeof(TEnum)}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GreaterThan(TValue target, TValue lower, string parameterName) where TValue : IComparable - { - if (target.CompareTo(lower) <= 0) - { - throw new ArgumentException($"Value must be greater than {lower}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GreaterEquals(TValue target, TValue lower, string parameterName) where TValue : IComparable - { - if (target.CompareTo(lower) < 0) - { - throw new ArgumentException($"Value must be greater or equal to {lower}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void LessThan(TValue target, TValue upper, string parameterName) where TValue : IComparable - { - if (target.CompareTo(upper) >= 0) - { - throw new ArgumentException($"Value must be less than {upper}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void LessEquals(TValue target, TValue upper, string parameterName) where TValue : IComparable - { - if (target.CompareTo(upper) > 0) - { - throw new ArgumentException($"Value must be less or equal to {upper}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotEmpty(IReadOnlyCollection enumerable, string parameterName) - { - NotNull(enumerable, parameterName); - - if (enumerable.Count == 0) - { - throw new ArgumentException("Collection does not contain an item.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotEmpty(Guid target, string parameterName) - { - if (target == Guid.Empty) - { - throw new ArgumentException("Value cannot be empty.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotNull(object target, string parameterName) - { - if (target == null) - { - throw new ArgumentNullException(parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotDefault(T target, string parameterName) - { - if (Equals(target, default(T))) - { - throw new ArgumentException("Value cannot be an the default value.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotNullOrEmpty(string target, string parameterName) - { - NotNull(target, parameterName); - - if (string.IsNullOrWhiteSpace(target)) - { - throw new ArgumentException("String parameter cannot be null or empty and cannot contain only blanks.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidFileName(string target, string parameterName) - { - NotNullOrEmpty(target, parameterName); - - if (target.Intersect(Path.GetInvalidFileNameChars()).Any()) - { - throw new ArgumentException("Value contains an invalid character.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Valid(IValidatable target, string parameterName, Func message) - { - NotNull(target, parameterName); - - target.Validate(message); - } - } -} diff --git a/src/Squidex.Infrastructure/Http/DumpFormatter.cs b/src/Squidex.Infrastructure/Http/DumpFormatter.cs deleted file mode 100644 index 815236fac..000000000 --- a/src/Squidex.Infrastructure/Http/DumpFormatter.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; - -namespace Squidex.Infrastructure.Http -{ - public static class DumpFormatter - { - public static string BuildDump(HttpRequestMessage request, HttpResponseMessage response, string responseBody) - { - return BuildDump(request, response, null, responseBody, TimeSpan.Zero); - } - - public static string BuildDump(HttpRequestMessage request, HttpResponseMessage response, string requestBody, string responseBody) - { - return BuildDump(request, response, requestBody, responseBody, TimeSpan.Zero); - } - - public static string BuildDump(HttpRequestMessage request, HttpResponseMessage response, string requestBody, string responseBody, TimeSpan elapsed, bool isTimeout = false) - { - var writer = new StringBuilder(); - - writer.AppendLine("Request:"); - writer.AppendRequest(request, requestBody); - - writer.AppendLine(); - writer.AppendLine(); - - writer.AppendLine("Response:"); - writer.AppendResponse(response, responseBody, elapsed, isTimeout); - - return writer.ToString(); - } - - private static void AppendRequest(this StringBuilder writer, HttpRequestMessage request, string requestBody) - { - var method = request.Method.ToString().ToUpperInvariant(); - - writer.AppendLine($"{method}: {request.RequestUri} HTTP/{request.Version}"); - - writer.AppendHeaders(request.Headers); - writer.AppendHeaders(request.Content?.Headers); - - if (!string.IsNullOrWhiteSpace(requestBody)) - { - writer.AppendLine(); - writer.AppendLine(requestBody); - } - } - - private static void AppendResponse(this StringBuilder writer, HttpResponseMessage response, string responseBody, TimeSpan elapsed, bool isTimeout) - { - if (response != null) - { - var responseCode = (int)response.StatusCode; - var responseText = Enum.GetName(typeof(HttpStatusCode), response.StatusCode); - - writer.AppendLine($"HTTP/{response.Version} {responseCode} {responseText}"); - - writer.AppendHeaders(response.Headers); - writer.AppendHeaders(response.Content?.Headers); - } - - if (!string.IsNullOrWhiteSpace(responseBody)) - { - writer.AppendLine(); - writer.AppendLine(responseBody); - } - - if (response != null && elapsed != TimeSpan.Zero) - { - writer.AppendLine(); - writer.AppendLine($"Elapsed: {elapsed}"); - } - - if (isTimeout) - { - writer.AppendLine($"Timeout after {elapsed}"); - } - } - - private static void AppendHeaders(this StringBuilder writer, HttpHeaders headers) - { - if (headers == null) - { - return; - } - - foreach (var header in headers) - { - writer.Append(header.Key); - writer.Append(": "); - writer.Append(string.Join("; ", header.Value)); - writer.AppendLine(); - } - } - } -} diff --git a/src/Squidex.Infrastructure/Json/IJsonSerializer.cs b/src/Squidex.Infrastructure/Json/IJsonSerializer.cs deleted file mode 100644 index 704ebaa78..000000000 --- a/src/Squidex.Infrastructure/Json/IJsonSerializer.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; - -namespace Squidex.Infrastructure.Json -{ - public interface IJsonSerializer - { - string Serialize(T value, bool intented = false); - - void Serialize(T value, Stream stream); - - T Deserialize(string value, Type actualType = null, Func stringConverter = null); - - T Deserialize(Stream stream, Type actualType = null, Func stringConverter = null); - } -} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs deleted file mode 100644 index a560e4abc..000000000 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs +++ /dev/null @@ -1,100 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public sealed class ConverterContractResolver : CamelCasePropertyNamesContractResolver - { - private readonly JsonConverter[] converters; - private readonly object lockObject = new object(); - private Dictionary converterCache = new Dictionary(); - - public ConverterContractResolver(params JsonConverter[] converters) - { - NamingStrategy = new CamelCaseNamingStrategy(false, true); - - this.converters = converters; - - foreach (var converter in converters) - { - if (converter is ISupportedTypes supportedTypes) - { - foreach (var type in supportedTypes.SupportedTypes) - { - converterCache[type] = converter; - } - } - } - } - - protected override JsonArrayContract CreateArrayContract(Type objectType) - { - if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IReadOnlyList<>)) - { - var implementationType = typeof(List<>).MakeGenericType(objectType.GetGenericArguments()); - - return base.CreateArrayContract(implementationType); - } - - return base.CreateArrayContract(objectType); - } - - protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) - { - if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)) - { - var implementationType = typeof(Dictionary<,>).MakeGenericType(objectType.GetGenericArguments()); - - return base.CreateDictionaryContract(implementationType); - } - - return base.CreateDictionaryContract(objectType); - } - - protected override JsonConverter ResolveContractConverter(Type objectType) - { - var result = base.ResolveContractConverter(objectType); - - if (result != null) - { - return result; - } - - var cache = converterCache; - - if (cache == null || !cache.TryGetValue(objectType, out result)) - { - foreach (var converter in converters) - { - if (converter.CanConvert(objectType)) - { - result = converter; - } - } - - lock (lockObject) - { - cache = converterCache; - - var updatedCache = (cache != null) - ? new Dictionary(cache) - : new Dictionary(); - updatedCache[objectType] = result; - - converterCache = updatedCache; - } - } - - return result; - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/InstantConverter.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/InstantConverter.cs deleted file mode 100644 index 9663accf9..000000000 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/InstantConverter.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using NodaTime; -using NodaTime.Text; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public sealed class InstantConverter : JsonConverter - { - public IEnumerable SupportedTypes - { - get - { - yield return typeof(Instant); - yield return typeof(Instant?); - } - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value != null) - { - writer.WriteValue(value.ToString()); - } - else - { - writer.WriteNull(); - } - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.String) - { - return InstantPattern.General.Parse(reader.Value.ToString()).Value; - } - - if (reader.TokenType == JsonToken.Date) - { - return Instant.FromDateTimeUtc((DateTime)reader.Value); - } - - if (reader.TokenType == JsonToken.Null && objectType == typeof(Instant?)) - { - return null; - } - - throw new JsonException($"Not a valid date time, expected String or Date, but got {reader.TokenType}."); - } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(Instant) || objectType == typeof(Instant?); - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs deleted file mode 100644 index 5bce0855f..000000000 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public abstract class JsonClassConverter : JsonConverter, ISupportedTypes where T : class - { - public virtual IEnumerable SupportedTypes - { - get { yield return typeof(T); } - } - - public sealed override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - { - return null; - } - - return ReadValue(reader, objectType, serializer); - } - - protected abstract T ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer); - - public sealed override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value == null) - { - writer.WriteNull(); - return; - } - - WriteValue(writer, (T)value, serializer); - } - - protected abstract void WriteValue(JsonWriter writer, T value, JsonSerializer serializer); - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(T); - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs deleted file mode 100644 index 49fdd5166..000000000 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs +++ /dev/null @@ -1,184 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Globalization; -using Newtonsoft.Json; -using Squidex.Infrastructure.Json.Objects; - -#pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public class JsonValueConverter : JsonConverter, ISupportedTypes - { - private readonly HashSet supportedTypes = new HashSet - { - typeof(IJsonValue), - typeof(JsonArray), - typeof(JsonBoolean), - typeof(JsonNull), - typeof(JsonNumber), - typeof(JsonObject), - typeof(JsonString) - }; - - public virtual IEnumerable SupportedTypes - { - get { return supportedTypes; } - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - return ReadJson(reader); - } - - private static IJsonValue ReadJson(JsonReader reader) - { - switch (reader.TokenType) - { - case JsonToken.Comment: - reader.Read(); - break; - case JsonToken.StartObject: - { - var result = JsonValue.Object(); - - while (reader.Read()) - { - switch (reader.TokenType) - { - case JsonToken.PropertyName: - var propertyName = reader.Value.ToString(); - - if (!reader.Read()) - { - throw new JsonSerializationException("Unexpected end when reading Object."); - } - - var value = ReadJson(reader); - - result[propertyName] = value; - break; - case JsonToken.EndObject: - return result; - } - } - - throw new JsonSerializationException("Unexpected end when reading Object."); - } - - case JsonToken.StartArray: - { - var result = JsonValue.Array(); - - while (reader.Read()) - { - switch (reader.TokenType) - { - case JsonToken.Comment: - break; - default: - var value = ReadJson(reader); - - result.Add(value); - break; - case JsonToken.EndArray: - return result; - } - } - - throw new JsonSerializationException("Unexpected end when reading Object."); - } - - case JsonToken.Integer: - return JsonValue.Create((long)reader.Value); - case JsonToken.Float: - return JsonValue.Create((double)reader.Value); - case JsonToken.Boolean: - return JsonValue.Create((bool)reader.Value); - case JsonToken.Date: - return JsonValue.Create(((DateTime)reader.Value).ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); - case JsonToken.String: - return JsonValue.Create(reader.Value.ToString()); - case JsonToken.Null: - case JsonToken.Undefined: - return JsonValue.Null; - } - - throw new NotSupportedException(); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value == null) - { - writer.WriteNull(); - return; - } - - WriteJson(writer, (IJsonValue)value); - } - - private static void WriteJson(JsonWriter writer, IJsonValue value) - { - switch (value) - { - case JsonNull _: - writer.WriteNull(); - break; - case JsonBoolean s: - writer.WriteValue(s.Value); - break; - case JsonString s: - writer.WriteValue(s.Value); - break; - case JsonNumber s: - - if (s.Value % 1 == 0) - { - writer.WriteValue((long)s.Value); - } - else - { - writer.WriteValue(s.Value); - } - - break; - case JsonArray array: - writer.WriteStartArray(); - - for (var i = 0; i < array.Count; i++) - { - WriteJson(writer, array[i]); - } - - writer.WriteEndArray(); - break; - - case JsonObject obj: - writer.WriteStartObject(); - - foreach (var kvp in obj) - { - writer.WritePropertyName(kvp.Key); - - WriteJson(writer, kvp.Value); - } - - writer.WriteEndObject(); - break; - } - } - - public override bool CanConvert(Type objectType) - { - return supportedTypes.Contains(objectType); - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs deleted file mode 100644 index 3a79cd5d5..000000000 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs +++ /dev/null @@ -1,100 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using Newtonsoft.Json; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public sealed class NewtonsoftJsonSerializer : IJsonSerializer - { - private readonly JsonSerializerSettings settings; - private readonly JsonSerializer serializer; - - private sealed class CustomReader : JsonTextReader - { - private readonly Func stringConverter; - - public override object Value - { - get - { - var value = base.Value; - - if (value is string s) - { - return stringConverter(s); - } - - return value; - } - } - - public CustomReader(TextReader reader, Func stringConverter) - : base(reader) - { - this.stringConverter = stringConverter; - } - } - - public NewtonsoftJsonSerializer(JsonSerializerSettings settings) - { - Guard.NotNull(settings, nameof(settings)); - - this.settings = settings; - - serializer = JsonSerializer.Create(settings); - } - - public string Serialize(T value, bool intented) - { - return JsonConvert.SerializeObject(value, intented ? Formatting.Indented : Formatting.None, settings); - } - - public void Serialize(T value, Stream stream) - { - using (var writer = new StreamWriter(stream)) - { - serializer.Serialize(writer, value); - - writer.Flush(); - } - } - - public T Deserialize(string value, Type actualType = null, Func stringConverter = null) - { - using (var textReader = new StringReader(value)) - { - actualType = actualType ?? typeof(T); - - using (var reader = GetReader(stringConverter, textReader)) - { - return (T)serializer.Deserialize(reader, actualType); - } - } - } - - public T Deserialize(Stream stream, Type actualType = null, Func stringConverter = null) - { - using (var textReader = new StreamReader(stream)) - { - actualType = actualType ?? typeof(T); - - using (var reader = GetReader(stringConverter, textReader)) - { - return (T)serializer.Deserialize(reader, actualType); - } - } - } - - private static JsonTextReader GetReader(Func stringConverter, TextReader textReader) - { - return stringConverter != null ? new CustomReader(textReader, stringConverter) : new JsonTextReader(textReader); - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs deleted file mode 100644 index 1e3ac6a94..000000000 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Newtonsoft.Json.Serialization; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public sealed class TypeNameSerializationBinder : DefaultSerializationBinder - { - private readonly TypeNameRegistry typeNameRegistry; - - public TypeNameSerializationBinder(TypeNameRegistry typeNameRegistry) - { - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); - - this.typeNameRegistry = typeNameRegistry; - } - - public override Type BindToType(string assemblyName, string typeName) - { - var type = typeNameRegistry.GetTypeOrNull(typeName); - - return type ?? base.BindToType(assemblyName, typeName); - } - - public override void BindToName(Type serializedType, out string assemblyName, out string typeName) - { - assemblyName = null; - - var name = typeNameRegistry.GetNameOrNull(serializedType); - - if (name != null) - { - typeName = name; - } - else - { - base.BindToName(serializedType, out assemblyName, out typeName); - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs b/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs deleted file mode 100644 index 743cf4a2a..000000000 --- a/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Json.Objects -{ - public interface IJsonValue : IEquatable - { - JsonValueType Type { get; } - - string ToJsonString(); - } -} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs b/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs deleted file mode 100644 index 50230c306..000000000 --- a/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; - -namespace Squidex.Infrastructure.Json.Objects -{ - public sealed class JsonArray : Collection, IJsonValue, IEquatable - { - public JsonValueType Type - { - get { return JsonValueType.Array; } - } - - public JsonArray() - { - } - - internal JsonArray(params object[] values) - : base(ToList(values)) - { - } - - private static List ToList(IEnumerable values) - { - return values?.Select(JsonValue.Create).ToList() ?? new List(); - } - - protected override void InsertItem(int index, IJsonValue item) - { - base.InsertItem(index, item ?? JsonValue.Null); - } - - protected override void SetItem(int index, IJsonValue item) - { - base.SetItem(index, item ?? JsonValue.Null); - } - - public override bool Equals(object obj) - { - return Equals(obj as JsonArray); - } - - public bool Equals(IJsonValue other) - { - return Equals(other as JsonArray); - } - - public bool Equals(JsonArray array) - { - if (array == null || array.Count != Count) - { - return false; - } - - for (var i = 0; i < Count; i++) - { - if (!this[i].Equals(array[i])) - { - return false; - } - } - - return true; - } - - public override int GetHashCode() - { - var hashCode = 17; - - for (var i = 0; i < Count; i++) - { - hashCode = (hashCode * 23) + this[i].GetHashCode(); - } - - return hashCode; - } - - public string ToJsonString() - { - return ToString(); - } - - public override string ToString() - { - return $"[{string.Join(", ", this.Select(x => x.ToJsonString()))}]"; - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs b/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs deleted file mode 100644 index 884462b3c..000000000 --- a/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Json.Objects -{ - public sealed class JsonNull : IJsonValue, IEquatable - { - public static readonly JsonNull Null = new JsonNull(); - - public JsonValueType Type - { - get { return JsonValueType.Null; } - } - - private JsonNull() - { - } - - public override bool Equals(object obj) - { - return Equals(obj as JsonNull); - } - - public bool Equals(IJsonValue other) - { - return Equals(other as JsonNull); - } - - public bool Equals(JsonNull other) - { - return other != null; - } - - public override int GetHashCode() - { - return 0; - } - - public string ToJsonString() - { - return ToString(); - } - - public override string ToString() - { - return "null"; - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs b/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs deleted file mode 100644 index 6eb6195d0..000000000 --- a/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs +++ /dev/null @@ -1,135 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace Squidex.Infrastructure.Json.Objects -{ - public class JsonObject : IReadOnlyDictionary, IJsonValue, IEquatable - { - private readonly Dictionary inner; - - public IJsonValue this[string key] - { - get - { - return inner[key]; - } - set - { - Guard.NotNull(key, nameof(key)); - - inner[key] = value ?? JsonValue.Null; - } - } - - public IEnumerable Keys - { - get { return inner.Keys; } - } - - public IEnumerable Values - { - get { return inner.Values; } - } - - public int Count - { - get { return inner.Count; } - } - - public JsonValueType Type - { - get { return JsonValueType.Array; } - } - - internal JsonObject() - { - inner = new Dictionary(); - } - - public JsonObject(JsonObject obj) - { - inner = new Dictionary(obj.inner); - } - - public JsonObject Add(string key, object value) - { - return Add(key, JsonValue.Create(value)); - } - - public JsonObject Add(string key, IJsonValue value) - { - inner[key] = value ?? JsonValue.Null; - - return this; - } - - public void Clear() - { - inner.Clear(); - } - - public bool Remove(string key) - { - return inner.Remove(key); - } - - public bool ContainsKey(string key) - { - return inner.ContainsKey(key); - } - - public bool TryGetValue(string key, out IJsonValue value) - { - return inner.TryGetValue(key, out value); - } - - public IEnumerator> GetEnumerator() - { - return inner.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return inner.GetEnumerator(); - } - - public override bool Equals(object obj) - { - return Equals(obj as JsonObject); - } - - public bool Equals(IJsonValue other) - { - return Equals(other as JsonObject); - } - - public bool Equals(JsonObject other) - { - return other != null && inner.EqualsDictionary(other.inner); - } - - public override int GetHashCode() - { - return inner.DictionaryHashCode(); - } - - public string ToJsonString() - { - return ToString(); - } - - public override string ToString() - { - return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value.ToJsonString()}"))}}}"; - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs b/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs deleted file mode 100644 index 5ea9c2bd7..000000000 --- a/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Json.Objects -{ - public abstract class JsonScalar : IJsonValue, IEquatable> - { - public abstract JsonValueType Type { get; } - - public T Value { get; } - - protected JsonScalar(T value) - { - Value = value; - } - - public override bool Equals(object obj) - { - return Equals(obj as JsonScalar); - } - - public bool Equals(IJsonValue other) - { - return Equals(other as JsonScalar); - } - - public bool Equals(JsonScalar other) - { - return other != null && other.Type == Type && Equals(other.Value, Value); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public override string ToString() - { - return Value.ToString(); - } - - public virtual string ToJsonString() - { - return ToString(); - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs b/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs deleted file mode 100644 index 922039f5e..000000000 --- a/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs +++ /dev/null @@ -1,136 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using NodaTime; - -#pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator - -namespace Squidex.Infrastructure.Json.Objects -{ - public static class JsonValue - { - public static readonly IJsonValue Empty = new JsonString(string.Empty); - - public static readonly IJsonValue True = JsonBoolean.True; - public static readonly IJsonValue False = JsonBoolean.False; - - public static readonly IJsonValue Null = JsonNull.Null; - - public static readonly IJsonValue Zero = new JsonNumber(0); - - public static JsonArray Array() - { - return new JsonArray(); - } - - public static JsonArray Array(params object[] values) - { - return new JsonArray(values); - } - - public static JsonObject Object() - { - return new JsonObject(); - } - - public static IJsonValue Create(object value) - { - if (value == null) - { - return Null; - } - - if (value is IJsonValue v) - { - return v; - } - - switch (value) - { - case string s: - return Create(s); - case bool b: - return Create(b); - case float f: - return Create(f); - case double d: - return Create(d); - case int i: - return Create(i); - case long l: - return Create(l); - case Instant i: - return Create(i); - } - - throw new ArgumentException("Invalid json type"); - } - - public static IJsonValue Create(bool value) - { - return value ? True : False; - } - - public static IJsonValue Create(double value) - { - Guard.ValidNumber(value, nameof(value)); - - if (value == 0) - { - return Zero; - } - - return new JsonNumber(value); - } - - public static IJsonValue Create(Instant? value) - { - if (value == null) - { - return Null; - } - - return Create(value.Value.ToString()); - } - - public static IJsonValue Create(double? value) - { - if (value == null) - { - return Null; - } - - return Create(value.Value); - } - - public static IJsonValue Create(bool? value) - { - if (value == null) - { - return Null; - } - - return Create(value.Value); - } - - public static IJsonValue Create(string value) - { - if (value == null) - { - return Null; - } - - if (value.Length == 0) - { - return Empty; - } - - return new JsonString(value); - } - } -} diff --git a/src/Squidex.Infrastructure/Language.cs b/src/Squidex.Infrastructure/Language.cs deleted file mode 100644 index 0d096fc31..000000000 --- a/src/Squidex.Infrastructure/Language.cs +++ /dev/null @@ -1,112 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; - -namespace Squidex.Infrastructure -{ - public sealed partial class Language - { - private static readonly Regex CultureRegex = new Regex("^([a-z]{2})(\\-[a-z]{2})?$", RegexOptions.IgnoreCase); - private static readonly Dictionary AllLanguagesField = new Dictionary(StringComparer.OrdinalIgnoreCase); - - internal static Language AddLanguage(string iso2Code, string englishName) - { - return AllLanguagesField.GetOrAdd(iso2Code, englishName, (c, n) => new Language(c, n)); - } - - public static Language GetLanguage(string iso2Code) - { - Guard.NotNullOrEmpty(iso2Code, nameof(iso2Code)); - - try - { - return AllLanguagesField[iso2Code]; - } - catch (KeyNotFoundException) - { - throw new NotSupportedException($"Language {iso2Code} is not supported"); - } - } - - public static IReadOnlyCollection AllLanguages - { - get { return AllLanguagesField.Values; } - } - - public string EnglishName { get; } - - public string Iso2Code { get; } - - private Language(string iso2Code, string englishName) - { - Iso2Code = iso2Code; - - EnglishName = englishName; - } - - public static bool IsValidLanguage(string iso2Code) - { - Guard.NotNullOrEmpty(iso2Code, nameof(iso2Code)); - - return AllLanguagesField.ContainsKey(iso2Code); - } - - public static bool TryGetLanguage(string iso2Code, out Language language) - { - Guard.NotNullOrEmpty(iso2Code, nameof(iso2Code)); - - return AllLanguagesField.TryGetValue(iso2Code, out language); - } - - public static implicit operator string(Language language) - { - return language?.Iso2Code; - } - - public static implicit operator Language(string iso2Code) - { - return GetLanguage(iso2Code); - } - - public static Language ParseOrNull(string input) - { - if (string.IsNullOrWhiteSpace(input)) - { - return null; - } - - input = input.Trim(); - - if (input.Length != 2) - { - var match = CultureRegex.Match(input); - - if (!match.Success) - { - return null; - } - - input = match.Groups[1].Value; - } - - if (TryGetLanguage(input.ToLowerInvariant(), out var result)) - { - return result; - } - - return null; - } - - public override string ToString() - { - return EnglishName; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/LanguagesInitializer.cs b/src/Squidex.Infrastructure/LanguagesInitializer.cs deleted file mode 100644 index 214e7aa60..000000000 --- a/src/Squidex.Infrastructure/LanguagesInitializer.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure -{ - public sealed class LanguagesInitializer : IInitializable - { - private readonly LanguagesOptions options; - - public LanguagesInitializer(IOptions options) - { - Guard.NotNull(options, nameof(options)); - - this.options = options.Value; - } - - public Task InitializeAsync(CancellationToken ct = default) - { - foreach (var kvp in options) - { - if (!string.IsNullOrWhiteSpace(kvp.Key) && !string.IsNullOrWhiteSpace(kvp.Value)) - { - Language.AddLanguage(kvp.Key, kvp.Value); - } - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs b/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs deleted file mode 100644 index 524145b0f..000000000 --- a/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Squidex.Infrastructure.Log.Adapter -{ - public class SemanticLogLoggerProvider : ILoggerProvider - { - private readonly IServiceProvider services; - private ISemanticLog log; - - public SemanticLogLoggerProvider(IServiceProvider services) - { - Guard.NotNull(services, nameof(services)); - - this.services = services; - } - - internal SemanticLogLoggerProvider(ISemanticLog log) - { - this.log = log; - } - - public static SemanticLogLoggerProvider ForTesting(ISemanticLog log) - { - return new SemanticLogLoggerProvider(log); - } - - public ILogger CreateLogger(string categoryName) - { - if (log == null && services != null) - { - log = services.GetService(typeof(ISemanticLog)) as ISemanticLog; - } - - if (log == null) - { - return NullLogger.Instance; - } - - return new SemanticLogLogger(log.CreateScope(writer => - { - writer.WriteProperty("category", categoryName); - })); - } - - public void Dispose() - { - } - } -} diff --git a/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs b/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs deleted file mode 100644 index 3f78d1efd..000000000 --- a/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Reflection; - -namespace Squidex.Infrastructure.Log -{ - public sealed class ApplicationInfoLogAppender : ILogAppender - { - private readonly string applicationName; - private readonly string applicationVersion; - private readonly string applicationSessionId; - - public ApplicationInfoLogAppender(Type type, Guid applicationSession) - : this(type?.Assembly, applicationSession) - { - } - - public ApplicationInfoLogAppender(Assembly assembly, Guid applicationSession) - { - Guard.NotNull(assembly, nameof(assembly)); - - applicationName = assembly.GetName().Name; - applicationVersion = assembly.GetName().Version.ToString(); - applicationSessionId = applicationSession.ToString(); - } - - public void Append(IObjectWriter writer, SemanticLogLevel logLevel) - { - writer.WriteObject("app", w => w - .WriteProperty("name", applicationName) - .WriteProperty("version", applicationVersion) - .WriteProperty("sessionId", applicationSessionId)); - } - } -} diff --git a/src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs b/src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs deleted file mode 100644 index 9ab7abeb2..000000000 --- a/src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Log -{ - public sealed class ConstantsLogWriter : ILogAppender - { - private readonly Action objectWriter; - - public ConstantsLogWriter(Action objectWriter) - { - Guard.NotNull(objectWriter, nameof(objectWriter)); - - this.objectWriter = objectWriter; - } - - public void Append(IObjectWriter writer, SemanticLogLevel logLevel) - { - objectWriter(writer); - } - } -} diff --git a/src/Squidex.Infrastructure/Log/FileChannel.cs b/src/Squidex.Infrastructure/Log/FileChannel.cs deleted file mode 100644 index 26a8392e6..000000000 --- a/src/Squidex.Infrastructure/Log/FileChannel.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure.Log.Internal; - -namespace Squidex.Infrastructure.Log -{ - public sealed class FileChannel : DisposableObjectBase, ILogChannel - { - private readonly FileLogProcessor processor; - private readonly object lockObject = new object(); - private volatile bool isInitialized; - - public FileChannel(string path) - { - Guard.NotNullOrEmpty(path, nameof(path)); - - processor = new FileLogProcessor(path); - } - - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - processor.Dispose(); - } - } - - public void Log(SemanticLogLevel logLevel, string message) - { - if (!isInitialized) - { - lock (lockObject) - { - if (!isInitialized) - { - processor.Initialize(); - - isInitialized = true; - } - } - } - - processor.EnqueueMessage(new LogMessageEntry { Message = message }); - } - } -} diff --git a/src/Squidex.Infrastructure/Log/IObjectWriter.cs b/src/Squidex.Infrastructure/Log/IObjectWriter.cs deleted file mode 100644 index 98f02aed3..000000000 --- a/src/Squidex.Infrastructure/Log/IObjectWriter.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using NodaTime; - -namespace Squidex.Infrastructure.Log -{ - public interface IObjectWriter - { - IObjectWriter WriteProperty(string property, string value); - - IObjectWriter WriteProperty(string property, double value); - - IObjectWriter WriteProperty(string property, long value); - - IObjectWriter WriteProperty(string property, bool value); - - IObjectWriter WriteProperty(string property, TimeSpan value); - - IObjectWriter WriteProperty(string property, Instant value); - - IObjectWriter WriteObject(string property, Action objectWriter); - - IObjectWriter WriteObject(string property, T context, Action objectWriter); - - IObjectWriter WriteArray(string property, Action arrayWriter); - - IObjectWriter WriteArray(string property, T context, Action arrayWriter); - } -} diff --git a/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs b/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs deleted file mode 100644 index 2957ceb63..000000000 --- a/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Threading; - -namespace Squidex.Infrastructure.Log.Internal -{ - public sealed class ConsoleLogProcessor : DisposableObjectBase - { - private const int MaxQueuedMessages = 1024; - private readonly IConsole console; - private readonly BlockingCollection messageQueue = new BlockingCollection(MaxQueuedMessages); - private readonly Thread outputThread; - - public ConsoleLogProcessor() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - console = new WindowsLogConsole(true); - } - else - { - console = new AnsiLogConsole(false); - } - - outputThread = new Thread(ProcessLogQueue) - { - IsBackground = true, Name = "Logging" - }; - outputThread.Start(); - } - - public void EnqueueMessage(LogMessageEntry message) - { - if (!messageQueue.IsAddingCompleted) - { - try - { - messageQueue.Add(message); - return; - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to enqueue log message: {ex}."); - } - } - - WriteMessage(message); - } - - private void ProcessLogQueue() - { - try - { - foreach (var message in messageQueue.GetConsumingEnumerable()) - { - WriteMessage(message); - } - } - catch - { - try - { - messageQueue.CompleteAdding(); - } - catch - { - return; - } - } - } - - private void WriteMessage(LogMessageEntry entry) - { - console.WriteLine(entry.Color, entry.Message); - } - - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - messageQueue.CompleteAdding(); - - try - { - outputThread.Join(1500); - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to shutdown log queue grateful: {ex}."); - } - finally - { - console.Reset(); - } - } - } - } -} diff --git a/src/Squidex.Infrastructure/Log/JsonLogWriter.cs b/src/Squidex.Infrastructure/Log/JsonLogWriter.cs deleted file mode 100644 index 13b0c1e91..000000000 --- a/src/Squidex.Infrastructure/Log/JsonLogWriter.cs +++ /dev/null @@ -1,225 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Text; -using System.Text.Json; -using NodaTime; - -namespace Squidex.Infrastructure.Log -{ - public sealed class JsonLogWriter : IObjectWriter, IArrayWriter - { - private readonly JsonWriterOptions formatting; - private readonly bool formatLine; - private readonly MemoryStream stream = new MemoryStream(); - private readonly StreamReader streamReader; - private Utf8JsonWriter jsonWriter; - - public long BufferSize - { - get { return stream.Length; } - } - - internal JsonLogWriter(JsonWriterOptions formatting, bool formatLine) - { - this.formatLine = formatLine; - this.formatting = formatting; - - streamReader = new StreamReader(stream, Encoding.UTF8); - - Start(); - } - - private void Start() - { - jsonWriter = new Utf8JsonWriter(stream, formatting); - jsonWriter.WriteStartObject(); - } - - internal void Reset() - { - stream.Position = 0; - stream.SetLength(0); - - Start(); - } - - IArrayWriter IArrayWriter.WriteValue(string value) - { - jsonWriter.WriteStringValue(value); - - return this; - } - - IArrayWriter IArrayWriter.WriteValue(double value) - { - jsonWriter.WriteNumberValue(value); - - return this; - } - - IArrayWriter IArrayWriter.WriteValue(long value) - { - jsonWriter.WriteNumberValue(value); - - return this; - } - - IArrayWriter IArrayWriter.WriteValue(bool value) - { - jsonWriter.WriteBooleanValue(value); - - return this; - } - - IArrayWriter IArrayWriter.WriteValue(Instant value) - { - jsonWriter.WriteStringValue(value.ToString()); - - return this; - } - - IArrayWriter IArrayWriter.WriteValue(TimeSpan value) - { - jsonWriter.WriteStringValue(value.ToString()); - - return this; - } - - IObjectWriter IObjectWriter.WriteProperty(string property, string value) - { - jsonWriter.WriteString(property, value); - - return this; - } - - IObjectWriter IObjectWriter.WriteProperty(string property, double value) - { - jsonWriter.WriteNumber(property, value); - - return this; - } - - IObjectWriter IObjectWriter.WriteProperty(string property, long value) - { - jsonWriter.WriteNumber(property, value); - - return this; - } - - IObjectWriter IObjectWriter.WriteProperty(string property, bool value) - { - jsonWriter.WriteBoolean(property, value); - - return this; - } - - IObjectWriter IObjectWriter.WriteProperty(string property, Instant value) - { - jsonWriter.WriteString(property, value.ToString()); - - return this; - } - - IObjectWriter IObjectWriter.WriteProperty(string property, TimeSpan value) - { - jsonWriter.WriteString(property, value.ToString()); - - return this; - } - - IObjectWriter IObjectWriter.WriteObject(string property, Action objectWriter) - { - jsonWriter.WritePropertyName(property); - jsonWriter.WriteStartObject(); - - objectWriter?.Invoke(this); - - jsonWriter.WriteEndObject(); - - return this; - } - - IObjectWriter IObjectWriter.WriteObject(string property, T context, Action objectWriter) - { - jsonWriter.WritePropertyName(property); - jsonWriter.WriteStartObject(); - - objectWriter?.Invoke(context, this); - - jsonWriter.WriteEndObject(); - - return this; - } - - IObjectWriter IObjectWriter.WriteArray(string property, Action arrayWriter) - { - jsonWriter.WritePropertyName(property); - jsonWriter.WriteStartArray(); - - arrayWriter?.Invoke(this); - - jsonWriter.WriteEndArray(); - - return this; - } - - IObjectWriter IObjectWriter.WriteArray(string property, T context, Action arrayWriter) - { - jsonWriter.WritePropertyName(property); - jsonWriter.WriteStartArray(); - - arrayWriter?.Invoke(context, this); - - jsonWriter.WriteEndArray(); - - return this; - } - - IArrayWriter IArrayWriter.WriteObject(Action objectWriter) - { - jsonWriter.WriteStartObject(); - - objectWriter?.Invoke(this); - - jsonWriter.WriteEndObject(); - - return this; - } - - IArrayWriter IArrayWriter.WriteObject(T context, Action objectWriter) - { - jsonWriter.WriteStartObject(); - - objectWriter?.Invoke(context, this); - - jsonWriter.WriteEndObject(); - - return this; - } - - public override string ToString() - { - jsonWriter.WriteEndObject(); - jsonWriter.Flush(); - - stream.Position = 0; - streamReader.DiscardBufferedData(); - - var json = streamReader.ReadToEnd(); - - if (formatLine) - { - json += Environment.NewLine; - } - - return json; - } - } -} diff --git a/src/Squidex.Infrastructure/Log/LockingLogStore.cs b/src/Squidex.Infrastructure/Log/LockingLogStore.cs deleted file mode 100644 index a5b7e02dc..000000000 --- a/src/Squidex.Infrastructure/Log/LockingLogStore.cs +++ /dev/null @@ -1,83 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Infrastructure.Log -{ - public sealed class LockingLogStore : ILogStore - { - private static readonly byte[] LockedText = Encoding.UTF8.GetBytes("Another process is currenty running, try it again later."); - private static readonly TimeSpan LockWaitingTime = TimeSpan.FromMinutes(1); - private readonly ILogStore inner; - private readonly ILockGrain lockGrain; - - public LockingLogStore(ILogStore inner, IGrainFactory grainFactory) - { - Guard.NotNull(inner, nameof(inner)); - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.inner = inner; - - lockGrain = grainFactory.GetGrain(SingleGrain.Id); - } - - public Task ReadLogAsync(string key, DateTime from, DateTime to, Stream stream) - { - return ReadLogAsync(key, from, to, stream, LockWaitingTime); - } - - public async Task ReadLogAsync(string key, DateTime from, DateTime to, Stream stream, TimeSpan lockTimeout) - { - using (var cts = new CancellationTokenSource(lockTimeout)) - { - string releaseToken = null; - - while (!cts.IsCancellationRequested) - { - releaseToken = await lockGrain.AcquireLockAsync(key); - - if (releaseToken != null) - { - break; - } - - try - { - await Task.Delay(2000, cts.Token); - } - catch (OperationCanceledException) - { - break; - } - } - - if (releaseToken != null) - { - try - { - await inner.ReadLogAsync(key, from, to, stream); - } - finally - { - await lockGrain.ReleaseLockAsync(releaseToken); - } - } - else - { - await stream.WriteAsync(LockedText, 0, LockedText.Length); - } - } - } - } -} diff --git a/src/Squidex.Infrastructure/Log/Profiler.cs b/src/Squidex.Infrastructure/Log/Profiler.cs deleted file mode 100644 index e2a5eed33..000000000 --- a/src/Squidex.Infrastructure/Log/Profiler.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.CompilerServices; -using System.Threading; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Log -{ - public delegate void ProfilerStarted(ProfilerSpan span); - - public static class Profiler - { - private static readonly AsyncLocal LocalSession = new AsyncLocal(); - private static readonly AsyncLocalCleaner Cleaner; - - public static ProfilerSession Session - { - get { return LocalSession.Value; } - } - - public static event ProfilerStarted SpanStarted; - - static Profiler() - { - Cleaner = new AsyncLocalCleaner(LocalSession); - } - - public static IDisposable StartSession() - { - LocalSession.Value = new ProfilerSession(); - - return Cleaner; - } - - public static IDisposable TraceMethod(Type type, [CallerMemberName] string memberName = null) - { - return Trace($"{type.Name}/{memberName}"); - } - - public static IDisposable TraceMethod([CallerMemberName] string memberName = null) - { - return Trace($"{typeof(T).Name}/{memberName}"); - } - - public static IDisposable TraceMethod(string objectName, [CallerMemberName] string memberName = null) - { - return Trace($"{objectName}/{memberName}"); - } - - public static IDisposable Trace(string key) - { - Guard.NotNull(key, nameof(key)); - - var session = LocalSession.Value; - - if (session == null) - { - return NoopDisposable.Instance; - } - - var span = new ProfilerSpan(session, key); - - SpanStarted?.Invoke(span); - - return span; - } - } -} diff --git a/src/Squidex.Infrastructure/Log/ProfilerSession.cs b/src/Squidex.Infrastructure/Log/ProfilerSession.cs deleted file mode 100644 index 6ecbd3d31..000000000 --- a/src/Squidex.Infrastructure/Log/ProfilerSession.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Concurrent; - -namespace Squidex.Infrastructure.Log -{ - public sealed class ProfilerSession - { - private struct ProfilerItem - { - public long Total; - public long Count; - } - - private readonly ConcurrentDictionary traces = new ConcurrentDictionary(); - - public void Measured(string name, long elapsed) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - traces.AddOrUpdate(name, x => - { - return new ProfilerItem { Total = elapsed, Count = 1 }; - }, - (x, result) => - { - result.Total += elapsed; - result.Count++; - - return result; - }); - } - - public void Write(IObjectWriter writer) - { - Guard.NotNull(writer, nameof(writer)); - - if (traces.Count > 0) - { - writer.WriteObject("profiler", p => - { - foreach (var kvp in traces) - { - p.WriteObject(kvp.Key, kvp.Value, (value, k) => k - .WriteProperty("elapsedMsTotal", value.Total) - .WriteProperty("elapsedMsAvg", value.Total / value.Count) - .WriteProperty("count", value.Count)); - } - }); - } - } - } -} diff --git a/src/Squidex.Infrastructure/Log/ProfilerSpan.cs b/src/Squidex.Infrastructure/Log/ProfilerSpan.cs deleted file mode 100644 index 9a2819ca2..000000000 --- a/src/Squidex.Infrastructure/Log/ProfilerSpan.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; - -namespace Squidex.Infrastructure.Log -{ - public sealed class ProfilerSpan : IDisposable - { - private readonly ProfilerSession session; - private readonly string key; - private ValueStopwatch watch = ValueStopwatch.StartNew(); - private List hooks; - - public string Key - { - get { return key; } - } - - public ProfilerSpan(ProfilerSession session, string key) - { - this.session = session; - - this.key = key; - } - - public void Listen(IDisposable hook) - { - Guard.NotNull(hook, nameof(hook)); - - if (hooks == null) - { - hooks = new List(1); - } - - hooks.Add(hook); - } - - public void Dispose() - { - var elapsedMs = watch.Stop(); - - session.Measured(key, elapsedMs); - - if (hooks != null) - { - for (var i = 0; i < hooks.Count; i++) - { - try - { - hooks[i].Dispose(); - } - catch - { - continue; - } - } - } - } - } -} diff --git a/src/Squidex.Infrastructure/Log/SemanticLog.cs b/src/Squidex.Infrastructure/Log/SemanticLog.cs deleted file mode 100644 index 3a040a231..000000000 --- a/src/Squidex.Infrastructure/Log/SemanticLog.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Squidex.Infrastructure.Log -{ - public sealed class SemanticLog : ISemanticLog - { - private readonly ILogChannel[] channels; - private readonly ILogAppender[] appenders; - private readonly IObjectWriterFactory writerFactory; - - public SemanticLog( - IEnumerable channels, - IEnumerable appenders, - IObjectWriterFactory writerFactory) - { - Guard.NotNull(channels, nameof(channels)); - Guard.NotNull(appenders, nameof(appenders)); - Guard.NotNull(writerFactory, nameof(writerFactory)); - - this.channels = channels.ToArray(); - this.appenders = appenders.ToArray(); - this.writerFactory = writerFactory; - } - - public void Log(SemanticLogLevel logLevel, T context, Action action) - { - Guard.NotNull(action, nameof(action)); - - var formattedText = FormatText(logLevel, context, action); - - LogFormattedText(logLevel, formattedText); - } - - private void LogFormattedText(SemanticLogLevel logLevel, string formattedText) - { - List exceptions = null; - - for (var i = 0; i < channels.Length; i++) - { - try - { - channels[i].Log(logLevel, formattedText); - } - catch (Exception ex) - { - if (exceptions == null) - { - exceptions = new List(); - } - - exceptions.Add(ex); - } - } - - if (exceptions != null && exceptions.Count > 0) - { - throw new AggregateException("An error occurred while writing to logger(s).", exceptions); - } - } - - private string FormatText(SemanticLogLevel logLevel, T context, Action objectWriter) - { - var writer = writerFactory.Create(); - - try - { - writer.WriteProperty(nameof(logLevel), logLevel.ToString()); - - objectWriter(context, writer); - - for (var i = 0; i < appenders.Length; i++) - { - appenders[i].Append(writer, logLevel); - } - - return writer.ToString(); - } - finally - { - writerFactory.Release(writer); - } - } - - public ISemanticLog CreateScope(Action objectWriter) - { - return new SemanticLog(channels, appenders.Union(new ILogAppender[] { new ConstantsLogWriter(objectWriter) }).ToArray(), writerFactory); - } - } -} diff --git a/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs b/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs deleted file mode 100644 index 1b58a9d02..000000000 --- a/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs +++ /dev/null @@ -1,189 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Log -{ - public static class SemanticLogExtensions - { - public static void LogTrace(this ISemanticLog log, Action objectWriter) - { - log.Log(SemanticLogLevel.Trace, null, (_, w) => objectWriter(w)); - } - - public static void LogTrace(this ISemanticLog log, T context, Action objectWriter) - { - log.Log(SemanticLogLevel.Trace, context, objectWriter); - } - - public static void LogDebug(this ISemanticLog log, Action objectWriter) - { - log.Log(SemanticLogLevel.Debug, null, (_, w) => objectWriter(w)); - } - - public static void LogDebug(this ISemanticLog log, T context, Action objectWriter) - { - log.Log(SemanticLogLevel.Debug, context, objectWriter); - } - - public static void LogInformation(this ISemanticLog log, Action objectWriter) - { - log.Log(SemanticLogLevel.Information, null, (_, w) => objectWriter(w)); - } - - public static void LogInformation(this ISemanticLog log, T context, Action objectWriter) - { - log.Log(SemanticLogLevel.Information, context, objectWriter); - } - - public static void LogWarning(this ISemanticLog log, Action objectWriter) - { - log.Log(SemanticLogLevel.Warning, null, (_, w) => objectWriter(w)); - } - - public static void LogWarning(this ISemanticLog log, T context, Action objectWriter) - { - log.Log(SemanticLogLevel.Warning, context, objectWriter); - } - - public static void LogWarning(this ISemanticLog log, Exception exception, Action objectWriter = null) - { - log.Log(SemanticLogLevel.Warning, null, (_, w) => w.WriteException(exception, objectWriter)); - } - - public static void LogWarning(this ISemanticLog log, Exception exception, T context, Action objectWriter = null) - { - log.Log(SemanticLogLevel.Warning, context, (ctx, w) => w.WriteException(exception, ctx, objectWriter)); - } - - public static void LogError(this ISemanticLog log, Action objectWriter) - { - log.Log(SemanticLogLevel.Error, null, (_, w) => objectWriter(w)); - } - - public static void LogError(this ISemanticLog log, T context, Action objectWriter) - { - log.Log(SemanticLogLevel.Error, context, objectWriter); - } - - public static void LogError(this ISemanticLog log, Exception exception, Action objectWriter = null) - { - log.Log(SemanticLogLevel.Error, null, (_, w) => w.WriteException(exception, objectWriter)); - } - - public static void LogError(this ISemanticLog log, Exception exception, T context, Action objectWriter = null) - { - log.Log(SemanticLogLevel.Error, context, (ctx, w) => w.WriteException(exception, ctx, objectWriter)); - } - - public static void LogFatal(this ISemanticLog log, Action objectWriter) - { - log.Log(SemanticLogLevel.Fatal, null, (_, w) => objectWriter(w)); - } - - public static void LogFatal(this ISemanticLog log, T context, Action objectWriter) - { - log.Log(SemanticLogLevel.Fatal, context, objectWriter); - } - - public static void LogFatal(this ISemanticLog log, Exception exception, Action objectWriter = null) - { - log.Log(SemanticLogLevel.Fatal, null, (_, w) => w.WriteException(exception, objectWriter)); - } - - public static void LogFatal(this ISemanticLog log, Exception exception, T context, Action objectWriter = null) - { - log.Log(SemanticLogLevel.Fatal, context, (ctx, w) => w.WriteException(exception, ctx, objectWriter)); - } - - private static void WriteException(this IObjectWriter writer, Exception exception, Action objectWriter) - { - objectWriter?.Invoke(writer); - - if (exception != null) - { - writer.WriteException(exception); - } - } - - private static void WriteException(this IObjectWriter writer, Exception exception, T context, Action objectWriter) - { - objectWriter?.Invoke(context, writer); - - if (exception != null) - { - writer.WriteException(exception); - } - } - - public static IObjectWriter WriteException(this IObjectWriter writer, Exception exception) - { - return writer.WriteObject(nameof(exception), exception, (ctx, w) => - { - w.WriteProperty("type", ctx.GetType().FullName); - - if (ctx.Message != null) - { - w.WriteProperty("message", ctx.Message); - } - - if (ctx.StackTrace != null) - { - w.WriteProperty("stackTrace", ctx.StackTrace); - } - }); - } - - public static IDisposable MeasureTrace(this ISemanticLog log, Action objectWriter) - { - return log.Measure(SemanticLogLevel.Trace, null, (_, w) => objectWriter(w)); - } - - public static IDisposable MeasureTrace(this ISemanticLog log, T context, Action objectWriter) - { - return log.Measure(SemanticLogLevel.Trace, context, objectWriter); - } - - public static IDisposable MeasureDebug(this ISemanticLog log, Action objectWriter) - { - return log.Measure(SemanticLogLevel.Debug, null, (_, w) => objectWriter(w)); - } - - public static IDisposable MeasureDebug(this ISemanticLog log, T context, Action objectWriter) - { - return log.Measure(SemanticLogLevel.Debug, context, objectWriter); - } - - public static IDisposable MeasureInformation(this ISemanticLog log, Action objectWriter) - { - return log.Measure(SemanticLogLevel.Information, null, (_, w) => objectWriter(w)); - } - - public static IDisposable MeasureInformation(this ISemanticLog log, T context, Action objectWriter) - { - return log.Measure(SemanticLogLevel.Information, context, objectWriter); - } - - private static IDisposable Measure(this ISemanticLog log, SemanticLogLevel logLevel, T context, Action objectWriter) - { - var watch = ValueStopwatch.StartNew(); - - return new DelegateDisposable(() => - { - var elapsedMs = watch.Stop(); - - log.Log(logLevel, (Context: context, elapsedMs), (ctx, w) => - { - objectWriter?.Invoke(ctx.Context, w); - - w.WriteProperty("elapsedMs", elapsedMs); - }); - }); - } - } -} diff --git a/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs b/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs deleted file mode 100644 index 243be67d0..000000000 --- a/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; - -namespace Squidex.Infrastructure.Log -{ - public sealed class TimestampLogAppender : ILogAppender - { - private readonly IClock clock; - - public TimestampLogAppender(IClock clock = null) - { - this.clock = clock ?? SystemClock.Instance; - } - - public void Append(IObjectWriter writer, SemanticLogLevel logLevel) - { - writer.WriteProperty("timestamp", clock.GetCurrentInstant()); - } - } -} diff --git a/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs b/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs deleted file mode 100644 index 3992f953e..000000000 --- a/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; - -namespace Squidex.Infrastructure.Migrations -{ - public interface IMigrationPath - { - (int Version, IEnumerable Migrations) GetNext(int version); - } -} diff --git a/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs b/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs deleted file mode 100644 index 2b5253289..000000000 --- a/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.Serialization; - -namespace Squidex.Infrastructure.Migrations -{ - [Serializable] - public class MigrationFailedException : Exception - { - public string Name { get; } - - public MigrationFailedException(string name) - : base(FormatException(name)) - { - Name = name; - } - - public MigrationFailedException(string name, Exception inner) - : base(FormatException(name), inner) - { - Name = name; - } - - protected MigrationFailedException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - Name = info.GetString(nameof(Name)); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(Name), Name); - } - - private static string FormatException(string name) - { - return $"Failed to run migration '{name}'"; - } - } -} diff --git a/src/Squidex.Infrastructure/Migrations/Migrator.cs b/src/Squidex.Infrastructure/Migrations/Migrator.cs deleted file mode 100644 index 0748e5a1a..000000000 --- a/src/Squidex.Infrastructure/Migrations/Migrator.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.Migrations -{ - public sealed class Migrator - { - private readonly ISemanticLog log; - private readonly IMigrationStatus migrationStatus; - private readonly IMigrationPath migrationPath; - - public int LockWaitMs { get; set; } = 500; - - public Migrator(IMigrationStatus migrationStatus, IMigrationPath migrationPath, ISemanticLog log) - { - Guard.NotNull(migrationStatus, nameof(migrationStatus)); - Guard.NotNull(migrationPath, nameof(migrationPath)); - Guard.NotNull(log, nameof(log)); - - this.migrationStatus = migrationStatus; - this.migrationPath = migrationPath; - - this.log = log; - } - - public async Task MigrateAsync(CancellationToken ct = default) - { - var version = 0; - - try - { - while (!await migrationStatus.TryLockAsync()) - { - log.LogInformation(w => w - .WriteProperty("action", "Migrate") - .WriteProperty("mesage", $"Waiting {LockWaitMs}ms to acquire lock.")); - - await Task.Delay(LockWaitMs); - } - - version = await migrationStatus.GetVersionAsync(); - - while (!ct.IsCancellationRequested) - { - var (newVersion, migrations) = migrationPath.GetNext(version); - - if (migrations == null || !migrations.Any()) - { - break; - } - - foreach (var migration in migrations) - { - var name = migration.GetType().ToString(); - - log.LogInformation(w => w - .WriteProperty("action", "Migration") - .WriteProperty("status", "Started") - .WriteProperty("migrator", name)); - - try - { - using (log.MeasureInformation(w => w - .WriteProperty("action", "Migration") - .WriteProperty("status", "Completed") - .WriteProperty("migrator", name))) - { - await migration.UpdateAsync(); - } - } - catch (Exception ex) - { - log.LogFatal(ex, w => w - .WriteProperty("action", "Migration") - .WriteProperty("status", "Failed") - .WriteProperty("migrator", name)); - - throw new MigrationFailedException(name, ex); - } - } - - version = newVersion; - } - } - finally - { - await migrationStatus.UnlockAsync(version); - } - } - } -} diff --git a/src/Squidex.Infrastructure/NamedId.cs b/src/Squidex.Infrastructure/NamedId.cs deleted file mode 100644 index e0c8106be..000000000 --- a/src/Squidex.Infrastructure/NamedId.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure -{ - public static class NamedId - { - public static NamedId Of(T id, string name) - { - return new NamedId(id, name); - } - } -} diff --git a/src/Squidex.Infrastructure/NamedId{T}.cs b/src/Squidex.Infrastructure/NamedId{T}.cs deleted file mode 100644 index eeeab8ab9..000000000 --- a/src/Squidex.Infrastructure/NamedId{T}.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -#pragma warning disable RECS0108 // Warns about static fields in generic types - -namespace Squidex.Infrastructure -{ - public delegate bool Parser(string input, out T result); - - public sealed class NamedId : IEquatable> - { - private static readonly int GuidLength = Guid.Empty.ToString().Length; - - public T Id { get; } - - public string Name { get; } - - public NamedId(T id, string name) - { - Guard.NotNull(id, nameof(id)); - Guard.NotNull(name, nameof(name)); - - Id = id; - - Name = name; - } - - public override string ToString() - { - return $"{Id},{Name}"; - } - - public override bool Equals(object obj) - { - return Equals(obj as NamedId); - } - - public bool Equals(NamedId other) - { - return other != null && (ReferenceEquals(this, other) || (Id.Equals(other.Id) && Name.Equals(other.Name))); - } - - public override int GetHashCode() - { - return (Id.GetHashCode() * 397) ^ Name.GetHashCode(); - } - - public static bool TryParse(string value, Parser parser, out NamedId result) - { - if (value != null) - { - if (typeof(T) == typeof(Guid)) - { - if (value.Length > GuidLength + 1 && value[GuidLength] == ',') - { - if (parser(value.Substring(0, GuidLength), out var id)) - { - result = new NamedId(id, value.Substring(GuidLength + 1)); - - return true; - } - } - } - else - { - var index = value.IndexOf(','); - - if (index > 0 && index < value.Length - 1) - { - if (parser(value.Substring(0, index), out var id)) - { - result = new NamedId(id, value.Substring(index + 1)); - - return true; - } - } - } - } - - result = null; - - return false; - } - - public static NamedId Parse(string value, Parser parser) - { - if (!TryParse(value, parser, out var result)) - { - throw new ArgumentException("Named id must have at least 2 parts divided by commata.", nameof(value)); - } - - return result; - } - } -} diff --git a/src/Squidex.Infrastructure/None.cs b/src/Squidex.Infrastructure/None.cs deleted file mode 100644 index 76fe93d4a..000000000 --- a/src/Squidex.Infrastructure/None.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure -{ - public sealed class None - { - public static readonly Type Type = typeof(None); - - private None() - { - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs b/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs deleted file mode 100644 index d3aff54d9..000000000 --- a/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.Extensions.DependencyInjection; -using Orleans; -using Orleans.Runtime; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class ActivationLimit : IActivationLimit, IDeactivater - { - private readonly IGrainActivationContext context; - private readonly IActivationLimiter limiter; - private int maxActivations; - - public ActivationLimit(IGrainActivationContext context, IActivationLimiter limiter) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(limiter, nameof(limiter)); - - this.context = context; - this.limiter = limiter; - } - - public void ReportIAmAlive() - { - if (maxActivations > 0) - { - limiter.Register(context.GrainType, this, maxActivations); - } - } - - public void ReportIAmDead() - { - if (maxActivations > 0) - { - limiter.Unregister(context.GrainType, this); - } - } - - public void SetLimit(int activations, TimeSpan lifetime) - { - maxActivations = activations; - - context.ObservableLifecycle?.Subscribe("Limiter", GrainLifecycleStage.Activate, - ct => - { - var runtime = context.ActivationServices.GetRequiredService(); - - runtime.DelayDeactivation(context.GrainInstance, lifetime); - - ReportIAmAlive(); - - return TaskHelper.Done; - }, - ct => - { - ReportIAmDead(); - - return TaskHelper.Done; - }); - } - - void IDeactivater.Deactivate() - { - var runtime = context.ActivationServices.GetRequiredService(); - - runtime.DeactivateOnIdle(context.GrainInstance); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Orleans/GrainBase.cs b/src/Squidex.Infrastructure/Orleans/GrainBase.cs deleted file mode 100644 index 9ae6b7d76..000000000 --- a/src/Squidex.Infrastructure/Orleans/GrainBase.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.Extensions.DependencyInjection; -using Orleans; -using Orleans.Core; -using Orleans.Runtime; - -namespace Squidex.Infrastructure.Orleans -{ - public abstract class GrainBase : Grain - { - protected GrainBase() - { - } - - protected GrainBase(IGrainIdentity identity, IGrainRuntime runtime) - : base(identity, runtime) - { - } - - public void ReportIAmAlive() - { - var limit = ServiceProvider.GetService(); - - limit?.ReportIAmAlive(); - } - - public void ReportIAmDead() - { - var limit = ServiceProvider.GetService(); - - limit?.ReportIAmDead(); - } - - protected void TryDelayDeactivation(TimeSpan timeSpan) - { - try - { - DelayDeactivation(timeSpan); - } - catch (InvalidOperationException) - { - } - } - - protected void TryDeactivateOnIdle() - { - try - { - DeactivateOnIdle(); - } - catch (InvalidOperationException) - { - } - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs b/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs deleted file mode 100644 index cebe9623f..000000000 --- a/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Orleans; -using Orleans.Runtime; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class GrainBootstrap : IBackgroundProcess where T : IBackgroundGrain - { - private const int NumTries = 10; - private readonly IGrainFactory grainFactory; - - public GrainBootstrap(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public async Task StartAsync(CancellationToken ct = default) - { - for (var i = 1; i <= NumTries; i++) - { - ct.ThrowIfCancellationRequested(); - try - { - var grain = grainFactory.GetGrain(SingleGrain.Id); - - await grain.ActivateAsync(); - - return; - } - catch (OrleansException) - { - if (i == NumTries) - { - throw; - } - } - } - } - - public override string ToString() - { - return typeof(T).ToString(); - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/GrainState.cs b/src/Squidex.Infrastructure/Orleans/GrainState.cs deleted file mode 100644 index d688acabd..000000000 --- a/src/Squidex.Infrastructure/Orleans/GrainState.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Orleans; -using Orleans.Runtime; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class GrainState : IGrainState where T : class, new() - { - private readonly IGrainActivationContext context; - private IPersistence persistence; - - public T Value { get; set; } = new T(); - - public long Version - { - get { return persistence.Version; } - } - - public GrainState(IGrainActivationContext context) - { - Guard.NotNull(context, nameof(context)); - - this.context = context; - - context.ObservableLifecycle.Subscribe("Persistence", GrainLifecycleStage.SetupState, SetupAsync); - } - - public Task SetupAsync(CancellationToken ct = default) - { - if (ct.IsCancellationRequested) - { - return Task.CompletedTask; - } - - if (context.GrainIdentity.PrimaryKeyString != null) - { - var store = context.ActivationServices.GetService>(); - - persistence = store.WithSnapshots(GetType(), context.GrainIdentity.PrimaryKeyString, ApplyState); - } - else - { - var store = context.ActivationServices.GetService>(); - - persistence = store.WithSnapshots(GetType(), context.GrainIdentity.PrimaryKey, ApplyState); - } - - return persistence.ReadAsync(); - } - - private void ApplyState(T value) - { - Value = value; - } - - public Task ClearAsync() - { - Value = new T(); - - return persistence.DeleteAsync(); - } - - public Task WriteAsync() - { - return persistence.WriteSnapshotAsync(Value); - } - - public Task WriteEventAsync(Envelope envelope) - { - return persistence.WriteEventAsync(envelope); - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/ILockGrain.cs b/src/Squidex.Infrastructure/Orleans/ILockGrain.cs deleted file mode 100644 index 05b4f18c0..000000000 --- a/src/Squidex.Infrastructure/Orleans/ILockGrain.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Orleans; - -namespace Squidex.Infrastructure.Orleans -{ - public interface ILockGrain : IGrainWithStringKey - { - Task AcquireLockAsync(string key); - - Task ReleaseLockAsync(string releaseToken); - } -} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs b/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs deleted file mode 100644 index bca54d1a2..000000000 --- a/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public interface IUniqueNameIndexGrain - { - Task ReserveAsync(T id, string name); - - Task AddAsync(string token); - - Task CountAsync(); - - Task RemoveReservationAsync(string token); - - Task RemoveAsync(T id); - - Task RebuildAsync(Dictionary values); - - Task ClearAsync(); - - Task GetIdAsync(string name); - - Task> GetIdsAsync(string[] names); - - Task> GetIdsAsync(); - } -} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs b/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs deleted file mode 100644 index fdae2ceb2..000000000 --- a/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Orleans; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public class IdsIndexGrain : Grain, IIdsIndexGrain where TState : IdsIndexState, new() - { - private readonly IGrainState state; - - public IdsIndexGrain(IGrainState state) - { - Guard.NotNull(state, nameof(state)); - - this.state = state; - } - - public Task CountAsync() - { - return Task.FromResult(state.Value.Ids.Count); - } - - public Task RebuildAsync(HashSet ids) - { - state.Value = new TState { Ids = ids }; - - return state.WriteAsync(); - } - - public Task AddAsync(T id) - { - state.Value.Ids.Add(id); - - return state.WriteAsync(); - } - - public Task RemoveAsync(T id) - { - state.Value.Ids.Remove(id); - - return state.WriteAsync(); - } - - public Task ClearAsync() - { - return state.ClearAsync(); - } - - public Task> GetIdsAsync() - { - return Task.FromResult(state.Value.Ids.ToList()); - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs b/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs deleted file mode 100644 index 5b71e57ca..000000000 --- a/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs +++ /dev/null @@ -1,136 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public class UniqueNameIndexGrain : Grain, IUniqueNameIndexGrain where TState : UniqueNameIndexState, new() - { - private readonly Dictionary reservations = new Dictionary(); - private readonly IGrainState state; - - public UniqueNameIndexGrain(IGrainState state) - { - Guard.NotNull(state, nameof(state)); - - this.state = state; - } - - public Task CountAsync() - { - return Task.FromResult(state.Value.Names.Count); - } - - public Task ClearAsync() - { - reservations.Clear(); - - return state.ClearAsync(); - } - - public Task RebuildAsync(Dictionary names) - { - state.Value = new TState { Names = names }; - - return state.WriteAsync(); - } - - public Task ReserveAsync(T id, string name) - { - string token = default; - - if (!IsInUse(name) && !IsReserved(name)) - { - token = RandomHash.Simple(); - - reservations.Add(token, (name, id)); - } - - return Task.FromResult(token); - } - - public async Task AddAsync(string token) - { - if (reservations.TryGetValue(token ?? string.Empty, out var reservation)) - { - state.Value.Names.Add(reservation.Name, reservation.Id); - - await state.WriteAsync(); - - reservations.Remove(token); - - return true; - } - - return false; - } - - public Task RemoveReservationAsync(string token) - { - reservations.Remove(token ?? string.Empty); - - return TaskHelper.Done; - } - - public async Task RemoveAsync(T id) - { - var name = state.Value.Names.FirstOrDefault(x => Equals(x.Value, id)).Key; - - if (name != null) - { - state.Value.Names.Remove(name); - - await state.WriteAsync(); - } - } - - public Task> GetIdsAsync(string[] names) - { - var result = new List(); - - if (names != null) - { - foreach (var name in names) - { - if (state.Value.Names.TryGetValue(name, out var id)) - { - result.Add(id); - } - } - } - - return Task.FromResult(result); - } - - public Task GetIdAsync(string name) - { - state.Value.Names.TryGetValue(name, out var id); - - return Task.FromResult(id); - } - - public Task> GetIdsAsync() - { - return Task.FromResult(state.Value.Names.Values.ToList()); - } - - private bool IsInUse(string name) - { - return state.Value.Names.ContainsKey(name); - } - - private bool IsReserved(string name) - { - return reservations.Values.Any(x => x.Name == name); - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/J{T}.cs b/src/Squidex.Infrastructure/Orleans/J{T}.cs deleted file mode 100644 index 8946ef845..000000000 --- a/src/Squidex.Infrastructure/Orleans/J{T}.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Orleans.CodeGeneration; -using Orleans.Concurrency; -using Orleans.Serialization; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.Orleans -{ - [Immutable] - public struct J - { - public T Value { get; } - - public J(T value) - { - Value = value; - } - - public static implicit operator T(J value) - { - return value.Value; - } - - public static implicit operator J(T d) - { - return new J(d); - } - - public override string ToString() - { - return Value?.ToString() ?? string.Empty; - } - - public static Task> AsTask(T value) - { - return Task.FromResult>(value); - } - - [CopierMethod] - public static object Copy(object input, ICopyContext context) - { - return input; - } - - [SerializerMethod] - public static void Serialize(object input, ISerializationContext context, Type expected) - { - using (Profiler.TraceMethod(nameof(J))) - { - var jsonSerializer = GetSerializer(context); - - var stream = new StreamWriterWrapper(context.StreamWriter); - - jsonSerializer.Serialize(input, stream); - } - } - - [DeserializerMethod] - public static object Deserialize(Type expected, IDeserializationContext context) - { - using (Profiler.TraceMethod(nameof(J))) - { - var jsonSerializer = GetSerializer(context); - - var stream = new StreamReaderWrapper(context.StreamReader); - - return jsonSerializer.Deserialize(stream, expected); - } - } - - private static IJsonSerializer GetSerializer(ISerializerContext context) - { - try - { - return context?.ServiceProvider?.GetRequiredService() ?? J.DefaultSerializer; - } - catch - { - return J.DefaultSerializer; - } - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs b/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs deleted file mode 100644 index 842a0dad9..000000000 --- a/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure.Caching; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class LocalCacheFilter : IIncomingGrainCallFilter - { - private readonly ILocalCache localCache; - - public LocalCacheFilter(ILocalCache localCache) - { - Guard.NotNull(localCache, nameof(localCache)); - - this.localCache = localCache; - } - - public async Task Invoke(IIncomingGrainCallContext context) - { - if (!context.Grain.GetType().Namespace.StartsWith("Orleans", StringComparison.OrdinalIgnoreCase)) - { - using (localCache.StartContext()) - { - await context.Invoke(); - } - } - else - { - await context.Invoke(); - } - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/LockGrain.cs b/src/Squidex.Infrastructure/Orleans/LockGrain.cs deleted file mode 100644 index 5e57fabaa..000000000 --- a/src/Squidex.Infrastructure/Orleans/LockGrain.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class LockGrain : GrainOfString, ILockGrain - { - private readonly Dictionary locks = new Dictionary(); - - public Task AcquireLockAsync(string key) - { - string releaseToken = null; - - if (!locks.ContainsKey(key)) - { - releaseToken = Guid.NewGuid().ToString(); - - locks.Add(key, releaseToken); - } - - return Task.FromResult(releaseToken); - } - - public Task ReleaseLockAsync(string releaseToken) - { - var key = locks.FirstOrDefault(x => x.Value == releaseToken).Key; - - if (!string.IsNullOrWhiteSpace(key)) - { - locks.Remove(key); - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs b/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs deleted file mode 100644 index b7edc178c..000000000 --- a/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class LoggingFilter : IIncomingGrainCallFilter - { - private readonly ISemanticLog log; - - public LoggingFilter(ISemanticLog log) - { - Guard.NotNull(log, nameof(log)); - - this.log = log; - } - - public async Task Invoke(IIncomingGrainCallContext context) - { - try - { - await context.Invoke(); - } - catch (DomainException) - { - throw; - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "GrainInvoked") - .WriteProperty("status", "Failed") - .WriteProperty("grain", context.Grain.ToString()) - .WriteProperty("grainMethod", context.ImplementationMethod.ToString())); - - throw; - } - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs b/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs deleted file mode 100644 index 9284b2a9f..000000000 --- a/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs +++ /dev/null @@ -1,88 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using Orleans.Serialization; - -namespace Squidex.Infrastructure.Orleans -{ - internal sealed class StreamReaderWrapper : Stream - { - private readonly IBinaryTokenStreamReader reader; - - public override bool CanRead - { - get { return true; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanWrite - { - get { return false; } - } - - public override long Length - { - get { return reader.Length; } - } - - public override long Position - { - get - { - return reader.CurrentPosition; - } - set - { - throw new NotSupportedException(); - } - } - - public StreamReaderWrapper(IBinaryTokenStreamReader reader) - { - this.reader = reader; - } - - public override void Flush() - { - } - - public override int Read(byte[] buffer, int offset, int count) - { - var bytesLeft = reader.Length - reader.CurrentPosition; - - if (bytesLeft < count) - { - count = bytesLeft; - } - - reader.ReadByteArray(buffer, offset, count); - - return count; - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - } -} diff --git a/src/Squidex.Infrastructure/Plugins/PluginManager.cs b/src/Squidex.Infrastructure/Plugins/PluginManager.cs deleted file mode 100644 index cdae457ac..000000000 --- a/src/Squidex.Infrastructure/Plugins/PluginManager.cs +++ /dev/null @@ -1,124 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.Plugins -{ - public sealed class PluginManager - { - private readonly HashSet loadedPlugins = new HashSet(); - private readonly List<(string Plugin, string Action, Exception Exception)> exceptions = new List<(string, string, Exception)>(); - - public IReadOnlyCollection Plugins - { - get { return loadedPlugins; } - } - - public void Add(string name, Assembly assembly) - { - Guard.NotNull(assembly, nameof(assembly)); - - var pluginTypes = - assembly.GetTypes() - .Where(t => typeof(IPlugin).IsAssignableFrom(t)) - .Where(t => !t.IsAbstract); - - foreach (var pluginType in pluginTypes) - { - try - { - var plugin = (IPlugin)Activator.CreateInstance(pluginType); - - loadedPlugins.Add(plugin); - } - catch (Exception ex) - { - LogException(name, "Instantiating", ex); - } - } - } - - public void LogException(string plugin, string action, Exception exception) - { - Guard.NotNull(plugin, nameof(plugin)); - Guard.NotNull(action, nameof(action)); - Guard.NotNull(exception, nameof(exception)); - - exceptions.Add((plugin, action, exception)); - } - - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - Guard.NotNull(services, nameof(services)); - Guard.NotNull(config, nameof(config)); - - foreach (var plugin in loadedPlugins) - { - plugin.ConfigureServices(services, config); - } - } - - public void ConfigureBefore(IApplicationBuilder app) - { - Guard.NotNull(app, nameof(app)); - - foreach (var plugin in loadedPlugins.OfType()) - { - plugin.ConfigureBefore(app); - } - } - - public void ConfigureAfter(IApplicationBuilder app) - { - Guard.NotNull(app, nameof(app)); - - foreach (var plugin in loadedPlugins.OfType()) - { - plugin.ConfigureAfter(app); - } - } - - public void Log(ISemanticLog log) - { - Guard.NotNull(log, nameof(log)); - - if (loadedPlugins.Count > 0 || exceptions.Count > 0) - { - var status = exceptions.Count > 0 ? "CompletedWithErrors" : "Completed"; - - log.LogInformation(w => w - .WriteProperty("action", "pluginsLoaded") - .WriteProperty("status", status) - .WriteArray("errors", e => - { - foreach (var error in exceptions) - { - e.WriteObject(x => x - .WriteProperty("plugin", error.Plugin) - .WriteProperty("action", error.Action) - .WriteException(error.Exception)); - } - }) - .WriteArray("plugins", a => - { - foreach (var plugin in loadedPlugins) - { - a.WriteValue(plugin.GetType().ToString()); - } - })); - } - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/ClrFilter.cs b/src/Squidex.Infrastructure/Queries/ClrFilter.cs deleted file mode 100644 index c2cb7eaa4..000000000 --- a/src/Squidex.Infrastructure/Queries/ClrFilter.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Queries -{ - public static class ClrFilter - { - public static LogicalFilter And(params FilterNode[] filters) - { - return new LogicalFilter(LogicalFilterType.And, filters); - } - - public static LogicalFilter Or(params FilterNode[] filters) - { - return new LogicalFilter(LogicalFilterType.Or, filters); - } - - public static NegateFilter Not(FilterNode filter) - { - return new NegateFilter(filter); - } - - public static CompareFilter Eq(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.Equals, value); - } - - public static CompareFilter Ne(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.NotEquals, value); - } - - public static CompareFilter Lt(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.LessThan, value); - } - - public static CompareFilter Le(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.LessThanOrEqual, value); - } - - public static CompareFilter Gt(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.GreaterThan, value); - } - - public static CompareFilter Ge(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.GreaterThanOrEqual, value); - } - - public static CompareFilter Contains(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.Contains, value); - } - - public static CompareFilter EndsWith(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.EndsWith, value); - } - - public static CompareFilter StartsWith(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.StartsWith, value); - } - - public static CompareFilter Empty(PropertyPath path) - { - return Binary(path, CompareOperator.Empty, null); - } - - public static CompareFilter In(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.In, value); - } - - private static CompareFilter Binary(PropertyPath path, CompareOperator @operator, ClrValue value) - { - return new CompareFilter(path, @operator, value ?? ClrValue.Null); - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/ClrValue.cs b/src/Squidex.Infrastructure/Queries/ClrValue.cs deleted file mode 100644 index 1a0719486..000000000 --- a/src/Squidex.Infrastructure/Queries/ClrValue.cs +++ /dev/null @@ -1,140 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using NodaTime; - -namespace Squidex.Infrastructure.Queries -{ - public sealed class ClrValue - { - public static readonly ClrValue Null = new ClrValue(null, ClrValueType.Null, false); - - public object Value { get; } - - public ClrValueType ValueType { get; } - - public bool IsList { get; } - - private ClrValue(object value, ClrValueType valueType, bool isList) - { - Value = value; - ValueType = valueType; - - IsList = isList; - } - - public static implicit operator ClrValue(Instant value) - { - return new ClrValue(value, ClrValueType.Instant, false); - } - - public static implicit operator ClrValue(Guid value) - { - return new ClrValue(value, ClrValueType.Guid, false); - } - - public static implicit operator ClrValue(bool value) - { - return new ClrValue(value, ClrValueType.Boolean, false); - } - - public static implicit operator ClrValue(float value) - { - return new ClrValue(value, ClrValueType.Single, false); - } - - public static implicit operator ClrValue(double value) - { - return new ClrValue(value, ClrValueType.Double, false); - } - - public static implicit operator ClrValue(int value) - { - return new ClrValue(value, ClrValueType.Int32, false); - } - - public static implicit operator ClrValue(long value) - { - return new ClrValue(value, ClrValueType.Int64, false); - } - - public static implicit operator ClrValue(string value) - { - return value != null ? new ClrValue(value, ClrValueType.String, false) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.Instant, true) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.Guid, true) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.Boolean, true) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.Single, true) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.Double, true) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.Int32, true) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.Int64, true) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.String, true) : Null; - } - - public override string ToString() - { - if (Value is IList list) - { - return $"[{string.Join(", ", list.OfType().Select(ToString).ToArray())}]"; - } - - return ToString(Value); - } - - private static string ToString(object value) - { - if (value == null) - { - return "null"; - } - - if (value is string s) - { - return $"'{s.Replace("'", "\\'")}'"; - } - - return string.Format(CultureInfo.InvariantCulture, "{0}", value); - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/CompareFilter.cs b/src/Squidex.Infrastructure/Queries/CompareFilter.cs deleted file mode 100644 index 5b522e6c9..000000000 --- a/src/Squidex.Infrastructure/Queries/CompareFilter.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Queries -{ - public sealed class CompareFilter : FilterNode - { - public PropertyPath Path { get; } - - public CompareOperator Operator { get; } - - public TValue Value { get; } - - public CompareFilter(PropertyPath path, CompareOperator @operator, TValue value) - { - Guard.NotNull(path, nameof(path)); - Guard.NotNull(value, nameof(value)); - Guard.Enum(@operator, nameof(@operator)); - - Path = path; - - Operator = @operator; - - Value = value; - } - - public override T Accept(FilterNodeVisitor visitor) - { - return visitor.Visit(this); - } - - public override string ToString() - { - switch (Operator) - { - case CompareOperator.Contains: - return $"contains({Path}, {Value})"; - case CompareOperator.Empty: - return $"empty({Path})"; - case CompareOperator.EndsWith: - return $"endsWith({Path}, {Value})"; - case CompareOperator.StartsWith: - return $"startsWith({Path}, {Value})"; - case CompareOperator.Equals: - return $"{Path} == {Value}"; - case CompareOperator.NotEquals: - return $"{Path} != {Value}"; - case CompareOperator.GreaterThan: - return $"{Path} > {Value}"; - case CompareOperator.GreaterThanOrEqual: - return $"{Path} >= {Value}"; - case CompareOperator.LessThan: - return $"{Path} < {Value}"; - case CompareOperator.LessThanOrEqual: - return $"{Path} <= {Value}"; - case CompareOperator.In: - return $"{Path} in {Value}"; - default: - return string.Empty; - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Queries/FilterNode.cs b/src/Squidex.Infrastructure/Queries/FilterNode.cs deleted file mode 100644 index 8dd348ce7..000000000 --- a/src/Squidex.Infrastructure/Queries/FilterNode.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Queries -{ - public abstract class FilterNode - { - public abstract T Accept(FilterNodeVisitor visitor); - } -} diff --git a/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs b/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs deleted file mode 100644 index e54883014..000000000 --- a/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs +++ /dev/null @@ -1,163 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using Squidex.Infrastructure.Json.Newtonsoft; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Infrastructure.Queries.Json -{ - public sealed class FilterConverter : JsonClassConverter> - { - public override IEnumerable SupportedTypes - { - get - { - yield return typeof(CompareFilter); - yield return typeof(FilterNode); - yield return typeof(LogicalFilter); - yield return typeof(NegateFilter); - } - } - - public override bool CanConvert(Type objectType) - { - return SupportedTypes.Contains(objectType); - } - - protected override FilterNode ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) - { - if (reader.TokenType != JsonToken.StartObject) - { - throw new JsonException($"Expected StartObject, but got {reader.TokenType}."); - } - - FilterNode result = null; - - var comparePath = (PropertyPath)null; - var compareOperator = (CompareOperator)99; - var compareValue = (IJsonValue)null; - - while (reader.Read()) - { - switch (reader.TokenType) - { - case JsonToken.PropertyName: - var propertyName = reader.Value.ToString(); - - if (!reader.Read()) - { - throw new JsonSerializationException("Unexpected end when reading filter."); - } - - if (result != null) - { - throw new JsonSerializationException($"Unexpected property {propertyName}"); - } - - switch (propertyName.ToLowerInvariant()) - { - case "not": - var filter = serializer.Deserialize>(reader); - - result = new NegateFilter(filter); - break; - case "and": - var andFilters = serializer.Deserialize>>(reader); - - result = new LogicalFilter(LogicalFilterType.And, andFilters); - break; - case "or": - var orFilters = serializer.Deserialize>>(reader); - - result = new LogicalFilter(LogicalFilterType.Or, orFilters); - break; - case "path": - comparePath = serializer.Deserialize(reader); - break; - case "op": - compareOperator = ReadOperator(reader, serializer); - break; - case "value": - compareValue = serializer.Deserialize(reader); - break; - } - - break; - case JsonToken.Comment: - break; - case JsonToken.EndObject: - if (result != null) - { - return result; - } - - if (comparePath == null) - { - throw new JsonSerializationException("Path not defined."); - } - - if (compareValue == null && compareOperator != CompareOperator.Empty) - { - throw new JsonSerializationException("Value not defined."); - } - - if (!compareOperator.IsEnumValue()) - { - throw new JsonSerializationException("Operator not defined."); - } - - return new CompareFilter(comparePath, compareOperator, compareValue ?? JsonValue.Null); - } - } - - throw new JsonSerializationException("Unexpected end when reading filter."); - } - - private static CompareOperator ReadOperator(JsonReader reader, JsonSerializer serializer) - { - var value = serializer.Deserialize(reader); - - switch (value.ToLowerInvariant()) - { - case "eq": - return CompareOperator.Equals; - case "ne": - return CompareOperator.NotEquals; - case "lt": - return CompareOperator.LessThan; - case "le": - return CompareOperator.LessThanOrEqual; - case "gt": - return CompareOperator.GreaterThan; - case "ge": - return CompareOperator.GreaterThanOrEqual; - case "empty": - return CompareOperator.Empty; - case "contains": - return CompareOperator.Contains; - case "endswith": - return CompareOperator.EndsWith; - case "startswith": - return CompareOperator.StartsWith; - case "in": - return CompareOperator.In; - } - - throw new JsonSerializationException($"Unexpected compare operator, got {value}."); - } - - protected override void WriteValue(JsonWriter writer, FilterNode value, JsonSerializer serializer) - { - throw new NotSupportedException(); - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs b/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs deleted file mode 100644 index c6a506e08..000000000 --- a/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs +++ /dev/null @@ -1,84 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using NJsonSchema; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Infrastructure.Queries.Json -{ - public sealed class JsonFilterVisitor : FilterNodeVisitor, IJsonValue> - { - private readonly List errors; - private readonly JsonSchema schema; - - private JsonFilterVisitor(JsonSchema schema, List errors) - { - this.schema = schema; - - this.errors = errors; - } - - public static FilterNode Parse(FilterNode filter, JsonSchema schema, List errors) - { - var visitor = new JsonFilterVisitor(schema, errors); - - var parsed = filter.Accept(visitor); - - if (visitor.errors.Count > 0) - { - return null; - } - else - { - return parsed; - } - } - - public override FilterNode Visit(NegateFilter nodeIn) - { - return new NegateFilter(nodeIn.Accept(this)); - } - - public override FilterNode Visit(LogicalFilter nodeIn) - { - return new LogicalFilter(nodeIn.Type, nodeIn.Filters.Select(x => x.Accept(this)).ToList()); - } - - public override FilterNode Visit(CompareFilter nodeIn) - { - CompareFilter result = null; - - if (nodeIn.Path.TryGetProperty(schema, errors, out var property)) - { - var isValidOperator = OperatorValidator.IsAllowedOperator(property, nodeIn.Operator); - - if (!isValidOperator) - { - errors.Add($"{nodeIn.Operator} is not a valid operator for type {property.Type} at {nodeIn.Path}."); - } - - var value = ValueConverter.Convert(property, nodeIn.Value, nodeIn.Path, errors); - - if (value != null && isValidOperator) - { - if (value.IsList && nodeIn.Operator != CompareOperator.In) - { - errors.Add($"Array value is not allowed for '{nodeIn.Operator}' operator and path '{nodeIn.Path}'."); - } - - result = new CompareFilter(nodeIn.Path, nodeIn.Operator, value); - } - } - - result = result ?? new CompareFilter(nodeIn.Path, nodeIn.Operator, ClrValue.Null); - - return result; - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs b/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs deleted file mode 100644 index 2062f310d..000000000 --- a/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using NJsonSchema; - -namespace Squidex.Infrastructure.Queries.Json -{ - public static class PropertyPathValidator - { - public static bool TryGetProperty(this PropertyPath path, JsonSchema schema, List errors, out JsonSchema property) - { - foreach (var element in path) - { - var parent = schema.Reference ?? schema; - - if (parent.Properties.TryGetValue(element, out var p)) - { - schema = p; - } - else - { - if (!string.IsNullOrWhiteSpace(parent.Title)) - { - errors.Add($"'{element}' is not a property of '{parent.Title}'."); - } - else - { - errors.Add($"Path '{path}' does not point to a valid property in the model."); - } - - property = null; - - return false; - } - } - - property = schema; - - return true; - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs b/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs deleted file mode 100644 index afd17c24b..000000000 --- a/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using NJsonSchema; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Infrastructure.Queries.Json -{ - public static class QueryParser - { - public static ClrQuery Parse(this JsonSchema schema, string json, IJsonSerializer jsonSerializer) - { - var query = ParseFromJson(json, jsonSerializer); - - var result = SimpleMapper.Map(query, new ClrQuery()); - - var errors = new List(); - - ConvertSorting(schema, result, errors); - ConvertFilters(schema, result, errors, query); - - if (errors.Count > 0) - { - throw new ValidationException("Failed to parse json query", errors.Select(x => new ValidationError(x)).ToArray()); - } - - return result; - } - - private static void ConvertFilters(JsonSchema schema, ClrQuery result, List errors, Query query) - { - if (query.Filter != null) - { - var filter = JsonFilterVisitor.Parse(query.Filter, schema, errors); - - result.Filter = Optimizer.Optimize(filter); - } - } - - private static void ConvertSorting(JsonSchema schema, ClrQuery result, List errors) - { - if (result.Sort != null) - { - foreach (var sorting in result.Sort) - { - sorting.Path.TryGetProperty(schema, errors, out _); - } - } - } - - private static Query ParseFromJson(string json, IJsonSerializer jsonSerializer) - { - try - { - return jsonSerializer.Deserialize>(json); - } - catch (JsonException ex) - { - throw new ValidationException("Failed to parse json query.", new ValidationError(ex.Message)); - } - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs b/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs deleted file mode 100644 index 19f8812f7..000000000 --- a/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs +++ /dev/null @@ -1,238 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NJsonSchema; -using NodaTime; -using NodaTime.Text; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Infrastructure.Queries.Json -{ - public static class ValueConverter - { - private delegate bool Parser(List errors, PropertyPath path, IJsonValue value, out T result); - - private static readonly InstantPattern[] InstantPatterns = - { - InstantPattern.General, - InstantPattern.ExtendedIso, - InstantPattern.CreateWithInvariantCulture("yyyy-MM-dd") - }; - - public static ClrValue Convert(JsonSchema schema, IJsonValue value, PropertyPath path, List errors) - { - ClrValue result = null; - - switch (GetType(schema)) - { - case JsonObjectType.Boolean: - { - if (value is JsonArray jsonArray) - { - result = ParseArray(errors, path, jsonArray, TryParseBoolean); - } - else if (TryParseBoolean(errors, path, value, out var temp)) - { - result = temp; - } - - break; - } - - case JsonObjectType.Integer: - case JsonObjectType.Number: - { - if (value is JsonArray jsonArray) - { - result = ParseArray(errors, path, jsonArray, TryParseNumber); - } - else if (TryParseNumber(errors, path, value, out var temp)) - { - result = temp; - } - - break; - } - - case JsonObjectType.String: - { - if (schema.Format == JsonFormatStrings.Guid) - { - if (value is JsonArray jsonArray) - { - result = ParseArray(errors, path, jsonArray, TryParseGuid); - } - else if (TryParseGuid(errors, path, value, out var temp)) - { - result = temp; - } - } - else if (schema.Format == JsonFormatStrings.DateTime) - { - if (value is JsonArray jsonArray) - { - result = ParseArray(errors, path, jsonArray, TryParseDateTime); - } - else if (TryParseDateTime(errors, path, value, out var temp)) - { - result = temp; - } - } - else - { - if (value is JsonArray jsonArray) - { - result = ParseArray(errors, path, jsonArray, TryParseString); - } - else if (TryParseString(errors, path, value, out var temp)) - { - result = temp; - } - } - - break; - } - - default: - { - errors.Add($"Unsupported type {schema.Type} for {path}."); - break; - } - } - - return result; - } - - private static List ParseArray(List errors, PropertyPath path, JsonArray array, Parser parser) - { - var items = new List(); - - foreach (var item in array) - { - if (parser(errors, path, item, out var temp)) - { - items.Add(temp); - } - } - - return items; - } - - private static bool TryParseBoolean(List errors, PropertyPath path, IJsonValue value, out bool result) - { - result = default; - - if (value is JsonBoolean jsonBoolean) - { - result = jsonBoolean.Value; - - return true; - } - - errors.Add($"Expected Boolean for path '{path}', but got {value.Type}."); - - return false; - } - - private static bool TryParseNumber(List errors, PropertyPath path, IJsonValue value, out double result) - { - result = default; - - if (value is JsonNumber jsonNumber) - { - result = jsonNumber.Value; - - return true; - } - - errors.Add($"Expected Number for path '{path}', but got {value.Type}."); - - return false; - } - - private static bool TryParseString(List errors, PropertyPath path, IJsonValue value, out string result) - { - result = default; - - if (value is JsonString jsonString) - { - result = jsonString.Value; - - return true; - } - else if (value is JsonNull) - { - return true; - } - - errors.Add($"Expected String for path '{path}', but got {value.Type}."); - - return false; - } - - private static bool TryParseGuid(List errors, PropertyPath path, IJsonValue value, out Guid result) - { - result = default; - - if (value is JsonString jsonString) - { - if (Guid.TryParse(jsonString.Value, out result)) - { - return true; - } - - errors.Add($"Expected Guid String for path '{path}', but got invalid String."); - } - else - { - errors.Add($"Expected Guid String for path '{path}', but got {value.Type}."); - } - - return false; - } - - private static bool TryParseDateTime(List errors, PropertyPath path, IJsonValue value, out Instant result) - { - result = default; - - if (value is JsonString jsonString) - { - foreach (var pattern in InstantPatterns) - { - var parsed = pattern.Parse(jsonString.Value); - - if (parsed.Success) - { - result = parsed.Value; - - return true; - } - } - - errors.Add($"Expected ISO8601 DateTime String for path '{path}', but got invalid String."); - } - else - { - errors.Add($"Expected ISO8601 DateTime String for path '{path}', but got {value.Type}."); - } - - return false; - } - - private static JsonObjectType GetType(JsonSchema schema) - { - if (schema.Item != null) - { - return schema.Item.Type; - } - - return schema.Type; - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/LogicalFilter.cs b/src/Squidex.Infrastructure/Queries/LogicalFilter.cs deleted file mode 100644 index 1fc2a1416..000000000 --- a/src/Squidex.Infrastructure/Queries/LogicalFilter.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; - -namespace Squidex.Infrastructure.Queries -{ - public sealed class LogicalFilter : FilterNode - { - public IReadOnlyList> Filters { get; } - - public LogicalFilterType Type { get; } - - public LogicalFilter(LogicalFilterType type, IReadOnlyList> filters) - { - Guard.NotNull(filters, nameof(filters)); - Guard.Enum(type, nameof(type)); - - Filters = filters; - - Type = type; - } - - public override T Accept(FilterNodeVisitor visitor) - { - return visitor.Visit(this); - } - - public override string ToString() - { - return $"({string.Join(Type == LogicalFilterType.And ? " && " : " || ", Filters)})"; - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/NegateFilter.cs b/src/Squidex.Infrastructure/Queries/NegateFilter.cs deleted file mode 100644 index 09583a0f8..000000000 --- a/src/Squidex.Infrastructure/Queries/NegateFilter.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Queries -{ - public sealed class NegateFilter : FilterNode - { - public FilterNode Filter { get; } - - public NegateFilter(FilterNode filter) - { - Guard.NotNull(filter, nameof(filter)); - - Filter = filter; - } - - public override T Accept(FilterNodeVisitor visitor) - { - return visitor.Visit(this); - } - - public override string ToString() - { - return $"!({Filter})"; - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs b/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs deleted file mode 100644 index db3fb47b0..000000000 --- a/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs +++ /dev/null @@ -1,178 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using Microsoft.OData; -using Microsoft.OData.Edm; -using Microsoft.OData.UriParser; -using NodaTime; -using NodaTime.Text; - -namespace Squidex.Infrastructure.Queries.OData -{ - public sealed class ConstantWithTypeVisitor : QueryNodeVisitor - { - private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean); - private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset); - private static readonly IEdmPrimitiveType DoubleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Double); - private static readonly IEdmPrimitiveType GuidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid); - private static readonly IEdmPrimitiveType Int32Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); - private static readonly IEdmPrimitiveType Int64Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64); - private static readonly IEdmPrimitiveType SingleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Single); - private static readonly IEdmPrimitiveType StringType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String); - - private static readonly ConstantWithTypeVisitor Instance = new ConstantWithTypeVisitor(); - - private ConstantWithTypeVisitor() - { - } - - public static ClrValue Visit(QueryNode node) - { - return node.Accept(Instance); - } - - public override ClrValue Visit(ConvertNode nodeIn) - { - if (nodeIn.TypeReference.Definition == BooleanType) - { - var value = ConstantVisitor.Visit(nodeIn.Source); - - return bool.Parse(value.ToString()); - } - - if (nodeIn.TypeReference.Definition == GuidType) - { - var value = ConstantVisitor.Visit(nodeIn.Source); - - return Guid.Parse(value.ToString()); - } - - if (nodeIn.TypeReference.Definition == DateTimeType) - { - var value = ConstantVisitor.Visit(nodeIn.Source); - - return ParseInstant(value); - } - - if (ConstantVisitor.Visit(nodeIn.Source) == null) - { - return ClrValue.Null; - } - - throw new NotSupportedException(); - } - - public override ClrValue Visit(CollectionConstantNode nodeIn) - { - if (nodeIn.ItemType.Definition == DateTimeType) - { - return nodeIn.Collection.Select(x => ParseInstant(x.Value)).ToList(); - } - - if (nodeIn.ItemType.Definition == GuidType) - { - return nodeIn.Collection.Select(x => (Guid)x.Value).ToList(); - } - - if (nodeIn.ItemType.Definition == BooleanType) - { - return nodeIn.Collection.Select(x => (bool)x.Value).ToList(); - } - - if (nodeIn.ItemType.Definition == SingleType) - { - return nodeIn.Collection.Select(x => (float)x.Value).ToList(); - } - - if (nodeIn.ItemType.Definition == DoubleType) - { - return nodeIn.Collection.Select(x => (double)x.Value).ToList(); - } - - if (nodeIn.ItemType.Definition == Int32Type) - { - return nodeIn.Collection.Select(x => (int)x.Value).ToList(); - } - - if (nodeIn.ItemType.Definition == Int64Type) - { - return nodeIn.Collection.Select(x => (long)x.Value).ToList(); - } - - if (nodeIn.ItemType.Definition == StringType) - { - return nodeIn.Collection.Select(x => (string)x.Value).ToList(); - } - - throw new NotSupportedException(); - } - - public override ClrValue Visit(ConstantNode nodeIn) - { - if (nodeIn.TypeReference.Definition == BooleanType) - { - return (bool)nodeIn.Value; - } - - if (nodeIn.TypeReference.Definition == SingleType) - { - return (float)nodeIn.Value; - } - - if (nodeIn.TypeReference.Definition == DoubleType) - { - return (double)nodeIn.Value; - } - - if (nodeIn.TypeReference.Definition == Int32Type) - { - return (int)nodeIn.Value; - } - - if (nodeIn.TypeReference.Definition == Int64Type) - { - return (long)nodeIn.Value; - } - - if (nodeIn.TypeReference.Definition == StringType) - { - return (string)nodeIn.Value; - } - - throw new NotSupportedException(); - } - - private static Instant ParseInstant(object value) - { - if (value is DateTimeOffset dateTimeOffset) - { - return Instant.FromDateTimeOffset(dateTimeOffset.Add(dateTimeOffset.Offset)); - } - - if (value is DateTime dateTime) - { - return Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)); - } - - if (value is Date date) - { - return Instant.FromUtc(date.Year, date.Month, date.Day, 0, 0); - } - - var parseResult = InstantPattern.General.Parse(value.ToString()); - - if (!parseResult.Success) - { - throw new ODataException("Datetime is not in a valid format. Use ISO 8601"); - } - - return parseResult.Value; - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs b/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs deleted file mode 100644 index 47a5d0ed0..000000000 --- a/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using Microsoft.OData.Edm; -using Microsoft.OData.UriParser; - -namespace Squidex.Infrastructure.Queries.OData -{ - public static class EdmModelExtensions - { - static EdmModelExtensions() - { - CustomUriFunctions.AddCustomUriFunction("empty", - new FunctionSignatureWithReturnType( - EdmCoreModel.Instance.GetBoolean(false), - EdmCoreModel.Instance.GetString(true))); - } - - public static ODataUriParser ParseQuery(this IEdmModel model, string query) - { - if (!model.EntityContainer.EntitySets().Any()) - { - return null; - } - - query = query ?? string.Empty; - - var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last(); - - if (query.StartsWith("?", StringComparison.Ordinal)) - { - query = query.Substring(1); - } - - var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); - - return parser; - } - - public static ClrQuery ToQuery(this ODataUriParser parser) - { - var query = new ClrQuery(); - - if (parser != null) - { - parser.ParseTake(query); - parser.ParseSkip(query); - parser.ParseFilter(query); - parser.ParseSort(query); - } - - return query; - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/Optimizer.cs b/src/Squidex.Infrastructure/Queries/Optimizer.cs deleted file mode 100644 index 7a8cb170d..000000000 --- a/src/Squidex.Infrastructure/Queries/Optimizer.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; - -namespace Squidex.Infrastructure.Queries -{ - public sealed class Optimizer : TransformVisitor - { - private static readonly Optimizer Instance = new Optimizer(); - - private Optimizer() - { - } - - public static FilterNode Optimize(FilterNode source) - { - return source?.Accept(Instance); - } - - public override FilterNode Visit(LogicalFilter nodeIn) - { - var pruned = nodeIn.Filters.Select(x => x.Accept(this)).Where(x => x != null).ToList(); - - if (pruned.Count == 1) - { - return pruned[0]; - } - - if (pruned.Count == 0) - { - return null; - } - - return new LogicalFilter(nodeIn.Type, pruned); - } - - public override FilterNode Visit(NegateFilter nodeIn) - { - var pruned = nodeIn.Filter.Accept(this); - - if (pruned == null) - { - return null; - } - - if (pruned is CompareFilter comparison) - { - if (comparison.Operator == CompareOperator.Equals) - { - return new CompareFilter(comparison.Path, CompareOperator.NotEquals, comparison.Value); - } - - if (comparison.Operator == CompareOperator.NotEquals) - { - return new CompareFilter(comparison.Path, CompareOperator.Equals, comparison.Value); - } - } - - return new NegateFilter(pruned); - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs b/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs deleted file mode 100644 index ec10a2452..000000000 --- a/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; - -namespace Squidex.Infrastructure.Queries -{ - public sealed class PascalCasePathConverter : TransformVisitor - { - private static readonly PascalCasePathConverter Instance = new PascalCasePathConverter(); - - private PascalCasePathConverter() - { - } - - public static FilterNode Transform(FilterNode node) - { - return node.Accept(Instance); - } - - public override FilterNode Visit(CompareFilter nodeIn) - { - return new CompareFilter(nodeIn.Path.Select(x => x.ToPascalCase()).ToList(), nodeIn.Operator, nodeIn.Value); - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/PropertyPath.cs b/src/Squidex.Infrastructure/Queries/PropertyPath.cs deleted file mode 100644 index 552d9f5c1..000000000 --- a/src/Squidex.Infrastructure/Queries/PropertyPath.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Collections.ObjectModel; -using System.Linq; - -namespace Squidex.Infrastructure.Queries -{ - public sealed class PropertyPath : ReadOnlyCollection - { - private static readonly char[] Separators = { '.', '/' }; - - public PropertyPath(IList items) - : base(items) - { - if (items.Count == 0) - { - throw new ArgumentException("Path cannot be empty.", nameof(items)); - } - } - - public static implicit operator PropertyPath(string path) - { - return new PropertyPath(path?.Split(Separators, StringSplitOptions.RemoveEmptyEntries).ToList()); - } - - public static implicit operator PropertyPath(string[] path) - { - return new PropertyPath(path?.ToList()); - } - - public static implicit operator PropertyPath(List path) - { - return new PropertyPath(path); - } - - public static implicit operator PropertyPath(ImmutableList path) - { - return new PropertyPath(path); - } - - public override string ToString() - { - return string.Join(".", this); - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/Query.cs b/src/Squidex.Infrastructure/Queries/Query.cs deleted file mode 100644 index 83dc03ff3..000000000 --- a/src/Squidex.Infrastructure/Queries/Query.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; - -namespace Squidex.Infrastructure.Queries -{ - public class Query - { - public FilterNode Filter { get; set; } - - public string FullText { get; set; } - - public long Skip { get; set; } - - public long Take { get; set; } = long.MaxValue; - - public List Sort { get; set; } = new List(); - - public override string ToString() - { - var parts = new List(); - - if (Filter != null) - { - parts.Add($"Filter: {Filter}"); - } - - if (FullText != null) - { - parts.Add($"FullText: '{FullText.Replace("'", "\'")}'"); - } - - if (Skip > 0) - { - parts.Add($"Skip: {Skip}"); - } - - if (Take < long.MaxValue) - { - parts.Add($"Take: {Take}"); - } - - if (Sort.Count > 0) - { - parts.Add($"Sort: {string.Join(", ", Sort)}"); - } - - return string.Join("; ", parts); - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/SortNode.cs b/src/Squidex.Infrastructure/Queries/SortNode.cs deleted file mode 100644 index fa4e47919..000000000 --- a/src/Squidex.Infrastructure/Queries/SortNode.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Queries -{ - public sealed class SortNode - { - public PropertyPath Path { get; } - - public SortOrder Order { get; set; } - - public SortNode(PropertyPath path, SortOrder order) - { - Guard.NotNull(path, nameof(path)); - Guard.Enum(order, nameof(order)); - - Path = path; - - Order = order; - } - - public override string ToString() - { - var path = string.Join(".", Path); - - return $"{path} {Order}"; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Queries/TransformVisitor.cs b/src/Squidex.Infrastructure/Queries/TransformVisitor.cs deleted file mode 100644 index d71e20403..000000000 --- a/src/Squidex.Infrastructure/Queries/TransformVisitor.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; - -namespace Squidex.Infrastructure.Queries -{ - public abstract class TransformVisitor : FilterNodeVisitor, TValue> - { - public override FilterNode Visit(CompareFilter nodeIn) - { - return nodeIn; - } - - public override FilterNode Visit(LogicalFilter nodeIn) - { - return new LogicalFilter(nodeIn.Type, nodeIn.Filters.Select(x => x.Accept(this)).ToList()); - } - - public override FilterNode Visit(NegateFilter nodeIn) - { - return new NegateFilter(nodeIn.Filter.Accept(this)); - } - } -} diff --git a/src/Squidex.Infrastructure/RefToken.cs b/src/Squidex.Infrastructure/RefToken.cs deleted file mode 100644 index 1d01ba267..000000000 --- a/src/Squidex.Infrastructure/RefToken.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure -{ - public sealed class RefToken : IEquatable - { - public string Type { get; } - - public string Identifier { get; } - - public bool IsClient - { - get { return string.Equals(Type, RefTokenType.Client, StringComparison.OrdinalIgnoreCase); } - } - - public bool IsSubject - { - get { return string.Equals(Type, RefTokenType.Subject, StringComparison.OrdinalIgnoreCase); } - } - - public RefToken(string type, string identifier) - { - Guard.NotNullOrEmpty(type, nameof(type)); - Guard.NotNullOrEmpty(identifier, nameof(identifier)); - - Type = type.ToLowerInvariant(); - - Identifier = identifier; - } - - public override string ToString() - { - return $"{Type}:{Identifier}"; - } - - public override bool Equals(object obj) - { - return Equals(obj as RefToken); - } - - public bool Equals(RefToken other) - { - return other != null && (ReferenceEquals(this, other) || (Type.Equals(other.Type) && Identifier.Equals(other.Identifier))); - } - - public override int GetHashCode() - { - return (Type.GetHashCode() * 397) ^ Identifier.GetHashCode(); - } - - public static bool TryParse(string value, out RefToken result) - { - if (value != null) - { - var idx = value.IndexOf(':'); - - if (idx > 0 && idx < value.Length - 1) - { - result = new RefToken(value.Substring(0, idx), value.Substring(idx + 1)); - - return true; - } - } - - result = null; - - return false; - } - - public static RefToken Parse(string value) - { - if (!TryParse(value, out var result)) - { - throw new ArgumentException("Ref token must have more than 2 parts divided by colon.", nameof(value)); - } - - return result; - } - } -} diff --git a/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs b/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs deleted file mode 100644 index 7db4f572a..000000000 --- a/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Reflection -{ - public interface IPropertyAccessor - { - object Get(object target); - - void Set(object target, object value); - } -} diff --git a/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs b/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs deleted file mode 100644 index defd58e92..000000000 --- a/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs +++ /dev/null @@ -1,78 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Reflection; - -namespace Squidex.Infrastructure.Reflection -{ - public sealed class PropertiesTypeAccessor - { - private static readonly ConcurrentDictionary AccessorCache = new ConcurrentDictionary(); - private readonly Dictionary accessors = new Dictionary(); - private readonly List properties = new List(); - - public IEnumerable Properties - { - get - { - return properties; - } - } - - private PropertiesTypeAccessor(Type type) - { - var allProperties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); - - foreach (var property in allProperties) - { - accessors[property.Name] = new PropertyAccessor(type, property); - - properties.Add(property); - } - } - - public static PropertiesTypeAccessor Create(Type targetType) - { - Guard.NotNull(targetType, nameof(targetType)); - - return AccessorCache.GetOrAdd(targetType, x => new PropertiesTypeAccessor(x)); - } - - public void SetValue(object target, string propertyName, object value) - { - Guard.NotNull(target, "target"); - - var accessor = FindAccessor(propertyName); - - accessor.Set(target, value); - } - - public object GetValue(object target, string propertyName) - { - Guard.NotNull(target, nameof(target)); - - var accessor = FindAccessor(propertyName); - - return accessor.Get(target); - } - - private IPropertyAccessor FindAccessor(string propertyName) - { - Guard.NotNullOrEmpty(propertyName, nameof(propertyName)); - - if (!accessors.TryGetValue(propertyName, out var accessor)) - { - throw new ArgumentException("Property does not exist.", nameof(propertyName)); - } - - return accessor; - } - } -} diff --git a/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs b/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs deleted file mode 100644 index 456caccbd..000000000 --- a/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Reflection; - -namespace Squidex.Infrastructure.Reflection -{ - public sealed class PropertyAccessor : IPropertyAccessor - { - private sealed class PropertyWrapper : IPropertyAccessor - { - private readonly Func getMethod; - private readonly Action setMethod; - - public PropertyWrapper(PropertyInfo propertyInfo) - { - if (propertyInfo.CanRead) - { - getMethod = (Func)propertyInfo.GetGetMethod(true).CreateDelegate(typeof(Func)); - } - else - { - getMethod = x => throw new NotSupportedException(); - } - - if (propertyInfo.CanWrite) - { - setMethod = (Action)propertyInfo.GetSetMethod(true).CreateDelegate(typeof(Action)); - } - else - { - setMethod = (x, y) => throw new NotSupportedException(); - } - } - - public object Get(object source) - { - return getMethod((TObject)source); - } - - public void Set(object source, object value) - { - setMethod((TObject)source, (TValue)value); - } - } - - private readonly IPropertyAccessor internalAccessor; - - public PropertyAccessor(Type targetType, PropertyInfo propertyInfo) - { - Guard.NotNull(targetType, nameof(targetType)); - Guard.NotNull(propertyInfo, nameof(propertyInfo)); - - internalAccessor = (IPropertyAccessor)Activator.CreateInstance(typeof(PropertyWrapper<,>).MakeGenericType(propertyInfo.DeclaringType, propertyInfo.PropertyType), propertyInfo); - } - - public object Get(object target) - { - Guard.NotNull(target, nameof(target)); - - return internalAccessor.Get(target); - } - - public void Set(object target, object value) - { - Guard.NotNull(target, nameof(target)); - - internalAccessor.Set(target, value); - } - } -} diff --git a/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs b/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs deleted file mode 100644 index 7967e4da6..000000000 --- a/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs +++ /dev/null @@ -1,84 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; - -#pragma warning disable RECS0108 // Warns about static fields in generic types - -namespace Squidex.Infrastructure.Reflection -{ - public static class SimpleCopier - { - private struct PropertyMapper - { - private readonly IPropertyAccessor accessor; - private readonly Func converter; - - public PropertyMapper(IPropertyAccessor accessor, Func converter) - { - this.accessor = accessor; - this.converter = converter; - } - - public void MapProperty(object source, object target) - { - var value = converter(accessor.Get(source)); - - accessor.Set(target, value); - } - } - - private static class ClassCopier where T : class, new() - { - private static readonly List Mappers = new List(); - - static ClassCopier() - { - var type = typeof(T); - - foreach (var property in type.GetPublicProperties()) - { - if (!property.CanWrite || !property.CanRead) - { - continue; - } - - var accessor = new PropertyAccessor(type, property); - - if (property.PropertyType.Implements()) - { - Mappers.Add(new PropertyMapper(accessor, x => ((ICloneable)x)?.Clone())); - } - else - { - Mappers.Add(new PropertyMapper(accessor, x => x)); - } - } - } - - public static T CopyThis(T source) - { - var destination = new T(); - - foreach (var mapper in Mappers) - { - mapper.MapProperty(source, destination); - } - - return destination; - } - } - - public static T Copy(this T source) where T : class, new() - { - Guard.NotNull(source, nameof(source)); - - return ClassCopier.CopyThis(source); - } - } -} diff --git a/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs b/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs deleted file mode 100644 index 29890e85b..000000000 --- a/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs +++ /dev/null @@ -1,186 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; - -#pragma warning disable RECS0108 // Warns about static fields in generic types - -namespace Squidex.Infrastructure.Reflection -{ - public static class SimpleMapper - { - private sealed class StringConversionPropertyMapper : PropertyMapper - { - public StringConversionPropertyMapper( - IPropertyAccessor sourceAccessor, - IPropertyAccessor targetAccessor) - : base(sourceAccessor, targetAccessor) - { - } - - public override void MapProperty(object source, object target, CultureInfo culture) - { - var value = GetValue(source); - - SetValue(target, value?.ToString()); - } - } - - private sealed class ConversionPropertyMapper : PropertyMapper - { - private readonly Type targetType; - - public ConversionPropertyMapper( - IPropertyAccessor sourceAccessor, - IPropertyAccessor targetAccessor, - Type targetType) - : base(sourceAccessor, targetAccessor) - { - this.targetType = targetType; - } - - public override void MapProperty(object source, object target, CultureInfo culture) - { - var value = GetValue(source); - - if (value == null) - { - return; - } - - try - { - var converted = Convert.ChangeType(value, targetType, culture); - - SetValue(target, converted); - } - catch - { - return; - } - } - } - - private class PropertyMapper - { - private readonly IPropertyAccessor sourceAccessor; - private readonly IPropertyAccessor targetAccessor; - - public PropertyMapper(IPropertyAccessor sourceAccessor, IPropertyAccessor targetAccessor) - { - this.sourceAccessor = sourceAccessor; - this.targetAccessor = targetAccessor; - } - - public virtual void MapProperty(object source, object target, CultureInfo culture) - { - var value = GetValue(source); - - SetValue(target, value); - } - - protected void SetValue(object destination, object value) - { - targetAccessor.Set(destination, value); - } - - protected object GetValue(object source) - { - return sourceAccessor.Get(source); - } - } - - private static class ClassMapper where TSource : class where TTarget : class - { - private static readonly List Mappers = new List(); - - static ClassMapper() - { - var sourceClassType = typeof(TSource); - var sourceProperties = - sourceClassType.GetPublicProperties() - .Where(x => x.CanRead).ToList(); - - var targetClassType = typeof(TTarget); - var targetProperties = - targetClassType.GetPublicProperties() - .Where(x => x.CanWrite).ToList(); - - foreach (var sourceProperty in sourceProperties) - { - var targetProperty = targetProperties.FirstOrDefault(x => x.Name == sourceProperty.Name); - - if (targetProperty == null) - { - continue; - } - - var sourceType = sourceProperty.PropertyType; - var targetType = targetProperty.PropertyType; - - if (sourceType == targetType) - { - Mappers.Add(new PropertyMapper( - new PropertyAccessor(sourceClassType, sourceProperty), - new PropertyAccessor(targetClassType, targetProperty))); - } - else if (targetType == typeof(string)) - { - Mappers.Add(new StringConversionPropertyMapper( - new PropertyAccessor(sourceClassType, sourceProperty), - new PropertyAccessor(targetClassType, targetProperty))); - } - else if (sourceType.Implements() || targetType.Implements()) - { - Mappers.Add(new ConversionPropertyMapper( - new PropertyAccessor(sourceClassType, sourceProperty), - new PropertyAccessor(targetClassType, targetProperty), - targetType)); - } - } - } - - public static TTarget MapClass(TSource source, TTarget destination, CultureInfo culture) - { - foreach (var mapper in Mappers) - { - mapper.MapProperty(source, destination, culture); - } - - return destination; - } - } - - public static TTarget Map(TSource source) - where TSource : class - where TTarget : class, new() - { - return Map(source, new TTarget(), CultureInfo.CurrentCulture); - } - - public static TTarget Map(TSource source, TTarget target) - where TSource : class - where TTarget : class - { - return Map(source, target, CultureInfo.CurrentCulture); - } - - public static TTarget Map(TSource source, TTarget target, CultureInfo culture) - where TSource : class - where TTarget : class - { - Guard.NotNull(source, nameof(source)); - Guard.NotNull(culture, nameof(culture)); - Guard.NotNull(target, nameof(target)); - - return ClassMapper.MapClass(source, target, culture); - } - } -} diff --git a/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs b/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs deleted file mode 100644 index 152be33ae..000000000 --- a/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs +++ /dev/null @@ -1,163 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Reflection; - -namespace Squidex.Infrastructure.Reflection -{ - public sealed class TypeNameRegistry - { - private readonly Dictionary namesByType = new Dictionary(); - private readonly Dictionary typesByName = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public TypeNameRegistry(IEnumerable providers = null) - { - if (providers != null) - { - foreach (var provider in providers) - { - Map(provider); - } - } - } - - public TypeNameRegistry MapObsolete(Type type, string name) - { - Guard.NotNull(type, nameof(type)); - Guard.NotNull(name, nameof(name)); - - lock (namesByType) - { - if (typesByName.TryGetValue(name, out var existingType) && existingType != type) - { - var message = $"The name '{name}' is already registered with type '{typesByName[name]}'"; - - throw new ArgumentException(message, nameof(type)); - } - - typesByName[name] = type; - } - - return this; - } - - public TypeNameRegistry Map(ITypeProvider provider) - { - Guard.NotNull(provider, nameof(provider)); - - provider.Map(this); - - return this; - } - - public TypeNameRegistry Map(Type type) - { - Guard.NotNull(type, nameof(type)); - - var typeNameAttribute = type.GetCustomAttribute(); - - if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName)) - { - Map(type, typeNameAttribute.TypeName); - } - - return this; - } - - public TypeNameRegistry Map(Type type, string name) - { - Guard.NotNull(type, nameof(type)); - Guard.NotNull(name, nameof(name)); - - lock (namesByType) - { - if (namesByType.TryGetValue(type, out var existingName) && existingName != name) - { - var message = $"The type '{type}' is already registered with name '{namesByType[type]}'"; - - throw new ArgumentException(message, nameof(type)); - } - - namesByType[type] = name; - - if (typesByName.TryGetValue(name, out var existingType) && existingType != type) - { - var message = $"The name '{name}' is already registered with type '{typesByName[name]}'"; - - throw new ArgumentException(message, nameof(type)); - } - - typesByName[name] = type; - } - - return this; - } - - public TypeNameRegistry MapUnmapped(Assembly assembly) - { - foreach (var type in assembly.GetTypes()) - { - if (!namesByType.ContainsKey(type)) - { - Map(type); - } - } - - return this; - } - - public string GetName() - { - return GetName(typeof(T)); - } - - public string GetNameOrNull() - { - return GetNameOrNull(typeof(T)); - } - - public string GetNameOrNull(Type type) - { - var result = namesByType.GetOrDefault(type); - - return result; - } - - public Type GetTypeOrNull(string name) - { - var result = typesByName.GetOrDefault(name); - - return result; - } - - public string GetName(Type type) - { - var result = namesByType.GetOrDefault(type); - - if (result == null) - { - throw new TypeNameNotFoundException($"There is no name for type '{type}"); - } - - return result; - } - - public Type GetType(string name) - { - var result = typesByName.GetOrDefault(name); - - if (result == null) - { - throw new TypeNameNotFoundException($"There is no type for name '{name}"); - } - - return result; - } - } -} diff --git a/src/Squidex.Infrastructure/RetryWindow.cs b/src/Squidex.Infrastructure/RetryWindow.cs deleted file mode 100644 index ed155d2d8..000000000 --- a/src/Squidex.Infrastructure/RetryWindow.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NodaTime; - -namespace Squidex.Infrastructure -{ - public sealed class RetryWindow - { - private readonly Duration windowDuration; - private readonly int windowSize; - private readonly Queue retries = new Queue(); - private readonly IClock clock; - - public RetryWindow(TimeSpan windowDuration, int windowSize, IClock clock = null) - { - this.windowDuration = Duration.FromTimeSpan(windowDuration); - this.windowSize = windowSize + 1; - - this.clock = clock ?? SystemClock.Instance; - } - - public void Reset() - { - retries.Clear(); - } - - public bool CanRetryAfterFailure() - { - var now = clock.GetCurrentInstant(); - - retries.Enqueue(now); - - while (retries.Count > windowSize) - { - retries.Dequeue(); - } - - return retries.Count < windowSize || (retries.Count > 0 && (now - retries.Peek()) > windowDuration); - } - } -} diff --git a/src/Squidex.Infrastructure/Security/Extensions.cs b/src/Squidex.Infrastructure/Security/Extensions.cs deleted file mode 100644 index b6b5e93f5..000000000 --- a/src/Squidex.Infrastructure/Security/Extensions.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Security.Claims; - -namespace Squidex.Infrastructure.Security -{ - public static class Extensions - { - public static RefToken Token(this ClaimsPrincipal principal) - { - var subjectId = principal.OpenIdSubject(); - - if (!string.IsNullOrWhiteSpace(subjectId)) - { - return new RefToken(RefTokenType.Subject, subjectId); - } - - var clientId = principal.OpenIdClientId(); - - if (!string.IsNullOrWhiteSpace(clientId)) - { - return new RefToken(RefTokenType.Client, clientId); - } - - return null; - } - - public static string OpenIdSubject(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Subject)?.Value; - } - - public static string OpenIdClientId(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value; - } - - public static string UserOrClientId(this ClaimsPrincipal principal) - { - return principal.OpenIdSubject() ?? principal.OpenIdClientId(); - } - - public static string OpenIdPreferredUserName(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.PreferredUserName)?.Value; - } - - public static string OpenIdName(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Name)?.Value; - } - - public static string OpenIdNickName(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.NickName)?.Value; - } - - public static string OpenIdEmail(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Email)?.Value; - } - - public static bool IsInClient(this ClaimsPrincipal principal, string client) - { - return principal.Claims.Any(x => x.Type == OpenIdClaims.ClientId && string.Equals(x.Value, client, StringComparison.OrdinalIgnoreCase)); - } - } -} diff --git a/src/Squidex.Infrastructure/Security/Permission.Part.cs b/src/Squidex.Infrastructure/Security/Permission.Part.cs deleted file mode 100644 index 47ecf2e5a..000000000 --- a/src/Squidex.Infrastructure/Security/Permission.Part.cs +++ /dev/null @@ -1,84 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; - -namespace Squidex.Infrastructure.Security -{ - public sealed partial class Permission - { - internal struct Part - { - private static readonly char[] AlternativeSeparators = { '|' }; - private static readonly char[] MainSeparators = { '.' }; - - public readonly string[] Alternatives; - - public readonly bool Exclusion; - - public Part(string[] alternatives, bool exclusion) - { - Alternatives = alternatives; - - Exclusion = exclusion; - } - - public static Part[] ParsePath(string path) - { - var parts = path.Split(MainSeparators, StringSplitOptions.RemoveEmptyEntries); - - var result = new Part[parts.Length]; - - for (var i = 0; i < result.Length; i++) - { - result[i] = Parse(parts[i]); - } - - return result; - } - - public static Part Parse(string part) - { - var isExclusion = false; - - if (part.StartsWith(Exclude, StringComparison.OrdinalIgnoreCase)) - { - isExclusion = true; - - part = part.Substring(1); - } - - string[] alternatives = null; - - if (part != Any) - { - alternatives = part.Split(AlternativeSeparators, StringSplitOptions.RemoveEmptyEntries); - } - - return new Part(alternatives, isExclusion); - } - - public static bool Intersects(ref Part lhs, ref Part rhs, bool allowNull) - { - if (lhs.Alternatives == null) - { - return true; - } - - if (allowNull && rhs.Alternatives == null) - { - return true; - } - - var shouldIntersect = !(lhs.Exclusion ^ rhs.Exclusion); - - return rhs.Alternatives != null && lhs.Alternatives.Intersect(rhs.Alternatives).Any() == shouldIntersect; - } - } - } -} diff --git a/src/Squidex.Infrastructure/Security/Permission.cs b/src/Squidex.Infrastructure/Security/Permission.cs deleted file mode 100644 index 19c6dc089..000000000 --- a/src/Squidex.Infrastructure/Security/Permission.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Security -{ - public sealed partial class Permission : IComparable, IEquatable - { - public const string Any = "*"; - public const string Exclude = "^"; - - private readonly string id; - private Part[] path; - - public string Id - { - get { return id; } - } - - private Part[] Path - { - get { return path ?? (path = Part.ParsePath(id)); } - } - - public Permission(string id) - { - Guard.NotNullOrEmpty(id, nameof(id)); - - this.id = id; - } - - public bool Allows(Permission permission) - { - if (permission == null) - { - return false; - } - - return Covers(Path, permission.Path); - } - - public bool Includes(Permission permission) - { - if (permission == null) - { - return false; - } - - return PartialCovers(Path, permission.Path); - } - - private static bool Covers(Part[] given, Part[] requested) - { - if (given.Length > requested.Length) - { - return false; - } - - for (var i = 0; i < given.Length; i++) - { - if (!Part.Intersects(ref given[i], ref requested[i], false)) - { - return false; - } - } - - return true; - } - - private static bool PartialCovers(Part[] given, Part[] requested) - { - for (var i = 0; i < Math.Min(given.Length, requested.Length); i++) - { - if (!Part.Intersects(ref given[i], ref requested[i], true)) - { - return false; - } - } - - return true; - } - - public bool StartsWith(string test) - { - return id.StartsWith(test, StringComparison.OrdinalIgnoreCase); - } - - public override bool Equals(object obj) - { - return Equals(obj as Permission); - } - - public bool Equals(Permission other) - { - return other != null && string.Equals(id, other.id, StringComparison.OrdinalIgnoreCase); - } - - public override int GetHashCode() - { - return id.GetHashCode(); - } - - public override string ToString() - { - return id; - } - - public int CompareTo(Permission other) - { - return other == null ? -1 : string.Compare(id, other.id, StringComparison.Ordinal); - } - } -} diff --git a/src/Squidex.Infrastructure/Security/PermissionSet.cs b/src/Squidex.Infrastructure/Security/PermissionSet.cs deleted file mode 100644 index 08f702b19..000000000 --- a/src/Squidex.Infrastructure/Security/PermissionSet.cs +++ /dev/null @@ -1,91 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace Squidex.Infrastructure.Security -{ - public sealed class PermissionSet : IReadOnlyCollection - { - public static readonly PermissionSet Empty = new PermissionSet(Array.Empty()); - - private readonly List permissions; - private readonly Lazy display; - - public int Count - { - get { return permissions.Count; } - } - - public PermissionSet(params Permission[] permissions) - : this((IEnumerable)permissions) - { - } - - public PermissionSet(params string[] permissions) - : this(permissions?.Select(x => new Permission(x))) - { - } - - public PermissionSet(IEnumerable permissions) - : this(permissions?.Select(x => new Permission(x))) - { - } - - public PermissionSet(IEnumerable permissions) - { - Guard.NotNull(permissions, nameof(permissions)); - - this.permissions = permissions.ToList(); - - display = new Lazy(() => string.Join(";", this.permissions)); - } - - public bool Allows(Permission other) - { - if (other == null) - { - return false; - } - - return permissions.Any(x => x.Allows(other)); - } - - public bool Includes(Permission other) - { - if (other == null) - { - return false; - } - - return permissions.Any(x => x.Includes(other)); - } - - public override string ToString() - { - return display.Value; - } - - public IEnumerable ToIds() - { - return permissions.Select(x => x.Id); - } - - public IEnumerator GetEnumerator() - { - return permissions.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return permissions.GetEnumerator(); - } - } -} diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj deleted file mode 100644 index aa339402d..000000000 --- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ /dev/null @@ -1,45 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs b/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs deleted file mode 100644 index cc7be1f10..000000000 --- a/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs +++ /dev/null @@ -1,45 +0,0 @@ - // ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Infrastructure.States -{ - public sealed class DefaultStreamNameResolver : IStreamNameResolver - { - private static readonly string[] Suffixes = { "Grain", "DomainObject", "State" }; - - public string GetStreamName(Type aggregateType, string id) - { - Guard.NotNullOrEmpty(id, nameof(id)); - Guard.NotNull(aggregateType, nameof(aggregateType)); - - return $"{aggregateType.TypeName(true, Suffixes)}-{id}"; - } - - public string WithNewId(string streamName, Func idGenerator) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNull(idGenerator, nameof(idGenerator)); - - var positionOfDash = streamName.IndexOf('-'); - - if (positionOfDash >= 0) - { - var newId = idGenerator(streamName.Substring(positionOfDash + 1)); - - if (!string.IsNullOrWhiteSpace(newId)) - { - streamName = $"{streamName.Substring(0, positionOfDash)}-{newId}"; - } - } - - return streamName; - } - } -} diff --git a/src/Squidex.Infrastructure/States/IStore.cs b/src/Squidex.Infrastructure/States/IStore.cs deleted file mode 100644 index 390e555de..000000000 --- a/src/Squidex.Infrastructure/States/IStore.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Infrastructure.States -{ - public delegate void HandleEvent(Envelope @event); - - public delegate void HandleSnapshot(T state); - - public interface IStore - { - IPersistence WithEventSourcing(Type owner, TKey key, HandleEvent applyEvent); - - IPersistence WithSnapshots(Type owner, TKey key, HandleSnapshot applySnapshot); - - IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, HandleSnapshot applySnapshot, HandleEvent applyEvent); - - ISnapshotStore GetSnapshotStore(); - } -} diff --git a/src/Squidex.Infrastructure/States/IStreamNameResolver.cs b/src/Squidex.Infrastructure/States/IStreamNameResolver.cs deleted file mode 100644 index 02b15f2fb..000000000 --- a/src/Squidex.Infrastructure/States/IStreamNameResolver.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.States -{ - public interface IStreamNameResolver - { - string GetStreamName(Type aggregateType, string id); - - string WithNewId(string streamName, Func idGenerator); - } -} diff --git a/src/Squidex.Infrastructure/States/InconsistentStateException.cs b/src/Squidex.Infrastructure/States/InconsistentStateException.cs deleted file mode 100644 index ccdd14a08..000000000 --- a/src/Squidex.Infrastructure/States/InconsistentStateException.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.Serialization; - -namespace Squidex.Infrastructure.States -{ - [Serializable] - public class InconsistentStateException : Exception - { - private readonly long currentVersion; - private readonly long expectedVersion; - - public long CurrentVersion - { - get { return currentVersion; } - } - - public long ExpectedVersion - { - get { return expectedVersion; } - } - - public InconsistentStateException(long currentVersion, long expectedVersion, Exception inner = null) - : base(FormatMessage(currentVersion, expectedVersion), inner) - { - this.currentVersion = currentVersion; - - this.expectedVersion = expectedVersion; - } - - protected InconsistentStateException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - currentVersion = info.GetInt64(nameof(currentVersion)); - - expectedVersion = info.GetInt64(nameof(expectedVersion)); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(currentVersion), currentVersion); - info.AddValue(nameof(expectedVersion), expectedVersion); - - base.GetObjectData(info, context); - } - - private static string FormatMessage(long currentVersion, long expectedVersion) - { - return $"Requested version {expectedVersion}, but found {currentVersion}."; - } - } -} diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs deleted file mode 100644 index df65d0ddc..000000000 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Infrastructure.States -{ - internal sealed class Persistence : Persistence, IPersistence - { - public Persistence(TKey ownerKey, Type ownerType, - IEventStore eventStore, - IEventEnricher eventEnricher, - IEventDataFormatter eventDataFormatter, - ISnapshotStore snapshotStore, - IStreamNameResolver streamNameResolver, - HandleEvent applyEvent) - : base(ownerKey, ownerType, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent) - { - } - } -} diff --git a/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs b/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs deleted file mode 100644 index c59303c17..000000000 --- a/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs +++ /dev/null @@ -1,241 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -#pragma warning disable RECS0012 // 'if' statement can be re-written as 'switch' statement - -namespace Squidex.Infrastructure.States -{ - internal class Persistence : IPersistence - { - private readonly TKey ownerKey; - private readonly Type ownerType; - private readonly ISnapshotStore snapshotStore; - private readonly IStreamNameResolver streamNameResolver; - private readonly IEventStore eventStore; - private readonly IEventEnricher eventEnricher; - private readonly IEventDataFormatter eventDataFormatter; - private readonly PersistenceMode persistenceMode; - private readonly HandleSnapshot applyState; - private readonly HandleEvent applyEvent; - private long versionSnapshot = EtagVersion.Empty; - private long versionEvents = EtagVersion.Empty; - private long version; - - public long Version - { - get { return version; } - } - - public Persistence(TKey ownerKey, Type ownerType, - IEventStore eventStore, - IEventEnricher eventEnricher, - IEventDataFormatter eventDataFormatter, - ISnapshotStore snapshotStore, - IStreamNameResolver streamNameResolver, - PersistenceMode persistenceMode, - HandleSnapshot applyState, - HandleEvent applyEvent) - { - this.ownerKey = ownerKey; - this.ownerType = ownerType; - this.applyState = applyState; - this.applyEvent = applyEvent; - this.eventStore = eventStore; - this.eventEnricher = eventEnricher; - this.eventDataFormatter = eventDataFormatter; - this.persistenceMode = persistenceMode; - this.snapshotStore = snapshotStore; - this.streamNameResolver = streamNameResolver; - } - - public async Task ReadAsync(long expectedVersion = EtagVersion.Any) - { - versionSnapshot = EtagVersion.Empty; - versionEvents = EtagVersion.Empty; - - await ReadSnapshotAsync(); - await ReadEventsAsync(); - - UpdateVersion(); - - if (expectedVersion > EtagVersion.Any && expectedVersion != version) - { - if (version == EtagVersion.Empty) - { - throw new DomainObjectNotFoundException(ownerKey.ToString(), ownerType); - } - else - { - throw new InconsistentStateException(version, expectedVersion); - } - } - } - - private async Task ReadSnapshotAsync() - { - if (UseSnapshots()) - { - var (state, position) = await snapshotStore.ReadAsync(ownerKey); - - if (position < EtagVersion.Empty) - { - position = EtagVersion.Empty; - } - - versionSnapshot = position; - versionEvents = position; - - if (applyState != null && position >= 0) - { - applyState(state); - } - } - } - - private async Task ReadEventsAsync() - { - if (UseEventSourcing()) - { - var events = await eventStore.QueryAsync(GetStreamName(), versionEvents + 1); - - foreach (var @event in events) - { - versionEvents++; - - if (@event.EventStreamNumber != versionEvents) - { - throw new InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps."); - } - - var parsedEvent = ParseKnownEvent(@event); - - if (applyEvent != null && parsedEvent != null) - { - applyEvent(parsedEvent); - } - } - } - } - - public async Task WriteSnapshotAsync(TSnapshot state) - { - var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1; - - if (newVersion != versionSnapshot) - { - await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion); - - versionSnapshot = newVersion; - } - - UpdateVersion(); - } - - public async Task WriteEventsAsync(IEnumerable> events) - { - Guard.NotNull(events, nameof(events)); - - var eventArray = events.ToArray(); - - if (eventArray.Length > 0) - { - var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any; - - var commitId = Guid.NewGuid(); - - foreach (var @event in eventArray) - { - eventEnricher.Enrich(@event, ownerKey); - } - - var eventStream = GetStreamName(); - var eventData = GetEventData(eventArray, commitId); - - try - { - await eventStore.AppendAsync(commitId, eventStream, expectedVersion, eventData); - } - catch (WrongEventVersionException ex) - { - throw new InconsistentStateException(ex.CurrentVersion, ex.ExpectedVersion, ex); - } - - versionEvents += eventArray.Length; - } - - UpdateVersion(); - } - - public async Task DeleteAsync() - { - if (UseEventSourcing()) - { - await eventStore.DeleteStreamAsync(GetStreamName()); - } - - if (UseSnapshots()) - { - await snapshotStore.RemoveAsync(ownerKey); - } - } - - private EventData[] GetEventData(Envelope[] events, Guid commitId) - { - return events.Map(x => eventDataFormatter.ToEventData(x, commitId, true)); - } - - private string GetStreamName() - { - return streamNameResolver.GetStreamName(ownerType, ownerKey.ToString()); - } - - private bool UseSnapshots() - { - return persistenceMode == PersistenceMode.Snapshots || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; - } - - private bool UseEventSourcing() - { - return persistenceMode == PersistenceMode.EventSourcing || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; - } - - private Envelope ParseKnownEvent(StoredEvent storedEvent) - { - try - { - return eventDataFormatter.Parse(storedEvent.Data); - } - catch (TypeNameNotFoundException) - { - return null; - } - } - - private void UpdateVersion() - { - if (persistenceMode == PersistenceMode.Snapshots) - { - version = versionSnapshot; - } - else if (persistenceMode == PersistenceMode.EventSourcing) - { - version = versionEvents; - } - else if (persistenceMode == PersistenceMode.SnapshotsAndEventSourcing) - { - version = Math.Max(versionEvents, versionSnapshot); - } - } - } -} diff --git a/src/Squidex.Infrastructure/States/Store.cs b/src/Squidex.Infrastructure/States/Store.cs deleted file mode 100644 index 2131258ec..000000000 --- a/src/Squidex.Infrastructure/States/Store.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Infrastructure.States -{ - public sealed class Store : IStore - { - private readonly IServiceProvider services; - private readonly IStreamNameResolver streamNameResolver; - private readonly IEventStore eventStore; - private readonly IEventEnricher eventEnricher; - private readonly IEventDataFormatter eventDataFormatter; - - public Store( - IEventStore eventStore, - IEventEnricher eventEnricher, - IEventDataFormatter eventDataFormatter, - IServiceProvider services, - IStreamNameResolver streamNameResolver) - { - this.eventStore = eventStore; - this.eventEnricher = eventEnricher; - this.eventDataFormatter = eventDataFormatter; - this.services = services; - this.streamNameResolver = streamNameResolver; - } - - public IPersistence WithEventSourcing(Type owner, TKey key, HandleEvent applyEvent) - { - return CreatePersistence(owner, key, applyEvent); - } - - public IPersistence WithSnapshots(Type owner, TKey key, HandleSnapshot applySnapshot) - { - return CreatePersistence(owner, key, PersistenceMode.Snapshots, applySnapshot, null); - } - - public IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, HandleSnapshot applySnapshot, HandleEvent applyEvent) - { - return CreatePersistence(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); - } - - private IPersistence CreatePersistence(Type owner, TKey key, HandleEvent applyEvent) - { - Guard.NotNull(key, nameof(key)); - - var snapshotStore = GetSnapshotStore(); - - return new Persistence(key, owner, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent); - } - - private IPersistence CreatePersistence(Type owner, TKey key, PersistenceMode mode, HandleSnapshot applySnapshot, HandleEvent applyEvent) - { - Guard.NotNull(key, nameof(key)); - - var snapshotStore = GetSnapshotStore(); - - return new Persistence(key, owner, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); - } - - public ISnapshotStore GetSnapshotStore() - { - return (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); - } - } -} diff --git a/src/Squidex.Infrastructure/StringExtensions.cs b/src/Squidex.Infrastructure/StringExtensions.cs deleted file mode 100644 index af47c2d17..000000000 --- a/src/Squidex.Infrastructure/StringExtensions.cs +++ /dev/null @@ -1,801 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -namespace Squidex.Infrastructure -{ - public static class StringExtensions - { - private const char NullChar = (char)0; - - private static readonly Regex SlugRegex = new Regex("^[a-z0-9]+(\\-[a-z0-9]+)*$", RegexOptions.Compiled); - private static readonly Regex EmailRegex = new Regex("^[a-zA-Z0-9.!#$%&’*+\\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$", RegexOptions.Compiled); - private static readonly Regex PropertyNameRegex = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled); - - private static readonly Dictionary LowerCaseDiacritics; - private static readonly Dictionary Diacritics = new Dictionary - { - ['$'] = "dollar", - ['%'] = "percent", - ['&'] = "and", - ['<'] = "less", - ['>'] = "greater", - ['|'] = "or", - ['¢'] = "cent", - ['£'] = "pound", - ['¤'] = "currency", - ['¥'] = "yen", - ['©'] = "(c)", - ['ª'] = "a", - ['®'] = "(r)", - ['º'] = "o", - ['À'] = "A", - ['Á'] = "A", - ['Â'] = "A", - ['Ã'] = "A", - ['Ä'] = "AE", - ['Å'] = "A", - ['Æ'] = "AE", - ['Ç'] = "C", - ['Ə'] = "E", - ['È'] = "E", - ['É'] = "E", - ['Ê'] = "E", - ['Ë'] = "E", - ['Ì'] = "I", - ['Í'] = "I", - ['Î'] = "I", - ['Ï'] = "I", - ['Ð'] = "D", - ['Ñ'] = "N", - ['Ò'] = "O", - ['Ó'] = "O", - ['Ô'] = "O", - ['Õ'] = "O", - ['Ö'] = "OE", - ['Ø'] = "O", - ['Ù'] = "U", - ['Ú'] = "U", - ['Û'] = "U", - ['Ü'] = "UE", - ['Ý'] = "Y", - ['Þ'] = "TH", - ['ß'] = "ss", - ['à'] = "a", - ['á'] = "a", - ['â'] = "a", - ['ã'] = "a", - ['ä'] = "ae", - ['å'] = "a", - ['æ'] = "ae", - ['ç'] = "c", - ['ə'] = "e", - ['è'] = "e", - ['é'] = "e", - ['ê'] = "e", - ['ë'] = "e", - ['ì'] = "i", - ['í'] = "i", - ['î'] = "i", - ['ï'] = "i", - ['ð'] = "d", - ['ñ'] = "n", - ['ò'] = "o", - ['ó'] = "o", - ['ô'] = "o", - ['õ'] = "o", - ['ö'] = "oe", - ['ø'] = "o", - ['ù'] = "u", - ['ú'] = "u", - ['û'] = "u", - ['ü'] = "ue", - ['ý'] = "y", - ['þ'] = "th", - ['ÿ'] = "y", - ['Ā'] = "A", - ['ā'] = "a", - ['Ă'] = "A", - ['ă'] = "a", - ['Ą'] = "A", - ['ą'] = "a", - ['Ć'] = "C", - ['ć'] = "c", - ['Č'] = "C", - ['č'] = "c", - ['Ď'] = "D", - ['ď'] = "d", - ['Đ'] = "DJ", - ['đ'] = "dj", - ['Ē'] = "E", - ['ē'] = "e", - ['Ė'] = "E", - ['ė'] = "e", - ['Ę'] = "e", - ['ę'] = "e", - ['Ě'] = "E", - ['ě'] = "e", - ['Ğ'] = "G", - ['ğ'] = "g", - ['Ģ'] = "G", - ['ģ'] = "g", - ['Ĩ'] = "I", - ['ĩ'] = "i", - ['Ī'] = "i", - ['ī'] = "i", - ['Į'] = "I", - ['į'] = "i", - ['İ'] = "I", - ['ı'] = "i", - ['Ķ'] = "k", - ['ķ'] = "k", - ['Ļ'] = "L", - ['ļ'] = "l", - ['Ľ'] = "L", - ['ľ'] = "l", - ['Ł'] = "L", - ['ł'] = "l", - ['Ń'] = "N", - ['ń'] = "n", - ['Ņ'] = "N", - ['ņ'] = "n", - ['Ň'] = "N", - ['ň'] = "n", - ['Ő'] = "O", - ['ő'] = "o", - ['Œ'] = "OE", - ['œ'] = "oe", - ['Ŕ'] = "R", - ['ŕ'] = "r", - ['Ř'] = "R", - ['ř'] = "r", - ['Ś'] = "S", - ['ś'] = "s", - ['Ş'] = "S", - ['ş'] = "s", - ['Š'] = "S", - ['š'] = "s", - ['Ţ'] = "T", - ['ţ'] = "t", - ['Ť'] = "T", - ['ť'] = "t", - ['Ũ'] = "U", - ['ũ'] = "u", - ['Ū'] = "u", - ['ū'] = "u", - ['Ů'] = "U", - ['ů'] = "u", - ['Ű'] = "U", - ['ű'] = "u", - ['Ų'] = "U", - ['ų'] = "u", - ['Ź'] = "Z", - ['ź'] = "z", - ['Ż'] = "Z", - ['ż'] = "z", - ['Ž'] = "Z", - ['ž'] = "z", - ['ƒ'] = "f", - ['Ơ'] = "O", - ['ơ'] = "o", - ['Ư'] = "U", - ['ư'] = "u", - ['Lj'] = "LJ", - ['lj'] = "lj", - ['Nj'] = "NJ", - ['nj'] = "nj", - ['Ș'] = "S", - ['ș'] = "s", - ['Ț'] = "T", - ['ț'] = "t", - ['˚'] = "o", - ['Ά'] = "A", - ['Έ'] = "E", - ['Ή'] = "H", - ['Ί'] = "I", - ['Ό'] = "O", - ['Ύ'] = "Y", - ['Ώ'] = "W", - ['ΐ'] = "i", - ['Α'] = "A", - ['Β'] = "B", - ['Γ'] = "G", - ['Δ'] = "D", - ['Ε'] = "E", - ['Ζ'] = "Z", - ['Η'] = "H", - ['Θ'] = "8", - ['Ι'] = "I", - ['Κ'] = "K", - ['Λ'] = "L", - ['Μ'] = "M", - ['Ν'] = "N", - ['Ξ'] = "3", - ['Ο'] = "O", - ['Π'] = "P", - ['Ρ'] = "R", - ['Σ'] = "S", - ['Τ'] = "T", - ['Υ'] = "Y", - ['Φ'] = "F", - ['Χ'] = "X", - ['Ψ'] = "PS", - ['Ω'] = "W", - ['Ϊ'] = "I", - ['Ϋ'] = "Y", - ['ά'] = "a", - ['έ'] = "e", - ['ή'] = "h", - ['ί'] = "i", - ['ΰ'] = "y", - ['α'] = "a", - ['β'] = "b", - ['γ'] = "g", - ['δ'] = "d", - ['ε'] = "e", - ['ζ'] = "z", - ['η'] = "h", - ['θ'] = "8", - ['ι'] = "i", - ['κ'] = "k", - ['λ'] = "l", - ['μ'] = "m", - ['ν'] = "n", - ['ξ'] = "3", - ['ο'] = "o", - ['π'] = "p", - ['ρ'] = "r", - ['ς'] = "s", - ['σ'] = "s", - ['τ'] = "t", - ['υ'] = "y", - ['φ'] = "f", - ['χ'] = "x", - ['ψ'] = "ps", - ['ω'] = "w", - ['ϊ'] = "i", - ['ϋ'] = "y", - ['ό'] = "o", - ['ύ'] = "y", - ['ώ'] = "w", - ['Ё'] = "Yo", - ['Ђ'] = "DJ", - ['Є'] = "Ye", - ['І'] = "I", - ['Ї'] = "Yi", - ['Ј'] = "J", - ['Љ'] = "LJ", - ['Њ'] = "NJ", - ['Ћ'] = "C", - ['Џ'] = "DZ", - ['А'] = "A", - ['Б'] = "B", - ['В'] = "V", - ['Г'] = "G", - ['Д'] = "D", - ['Е'] = "E", - ['Ж'] = "Zh", - ['З'] = "Z", - ['И'] = "I", - ['Й'] = "J", - ['К'] = "K", - ['Л'] = "L", - ['М'] = "M", - ['Н'] = "N", - ['О'] = "O", - ['П'] = "P", - ['Р'] = "R", - ['С'] = "S", - ['Т'] = "T", - ['У'] = "U", - ['Ф'] = "F", - ['Х'] = "H", - ['Ц'] = "C", - ['Ч'] = "Ch", - ['Ш'] = "Sh", - ['Щ'] = "Sh", - ['Ъ'] = "U", - ['Ы'] = "Y", - ['Ь'] = "b", - ['Э'] = "E", - ['Ю'] = "Yu", - ['Я'] = "Ya", - ['а'] = "a", - ['б'] = "b", - ['в'] = "v", - ['г'] = "g", - ['д'] = "d", - ['е'] = "e", - ['ж'] = "zh", - ['з'] = "z", - ['и'] = "i", - ['й'] = "j", - ['к'] = "k", - ['л'] = "l", - ['м'] = "m", - ['н'] = "n", - ['о'] = "o", - ['п'] = "p", - ['р'] = "r", - ['с'] = "s", - ['т'] = "t", - ['у'] = "u", - ['ф'] = "f", - ['х'] = "h", - ['ц'] = "c", - ['ч'] = "ch", - ['ш'] = "sh", - ['щ'] = "sh", - ['ъ'] = "u", - ['ы'] = "y", - ['ь'] = "s", - ['э'] = "e", - ['ю'] = "yu", - ['я'] = "ya", - ['ё'] = "yo", - ['ђ'] = "dj", - ['є'] = "ye", - ['і'] = "i", - ['ї'] = "yi", - ['ј'] = "j", - ['љ'] = "lj", - ['њ'] = "nj", - ['ћ'] = "c", - ['џ'] = "dz", - ['Ґ'] = "G", - ['ґ'] = "g", - ['฿'] = "baht", - ['ა'] = "a", - ['ბ'] = "b", - ['გ'] = "g", - ['დ'] = "d", - ['ე'] = "e", - ['ვ'] = "v", - ['ზ'] = "z", - ['თ'] = "t", - ['ი'] = "i", - ['კ'] = "k", - ['ლ'] = "l", - ['მ'] = "m", - ['ნ'] = "n", - ['ო'] = "o", - ['პ'] = "p", - ['ჟ'] = "zh", - ['რ'] = "r", - ['ს'] = "s", - ['ტ'] = "t", - ['უ'] = "u", - ['ფ'] = "f", - ['ქ'] = "k", - ['ღ'] = "gh", - ['ყ'] = "q", - ['შ'] = "sh", - ['ჩ'] = "ch", - ['ც'] = "ts", - ['ძ'] = "dz", - ['წ'] = "ts", - ['ჭ'] = "ch", - ['ხ'] = "kh", - ['ჯ'] = "j", - ['ჰ'] = "h", - ['ẞ'] = "SS", - ['Ạ'] = "A", - ['ạ'] = "a", - ['Ả'] = "A", - ['ả'] = "a", - ['Ấ'] = "A", - ['ấ'] = "a", - ['Ầ'] = "A", - ['ầ'] = "a", - ['Ẩ'] = "A", - ['ẩ'] = "a", - ['Ẫ'] = "A", - ['ẫ'] = "a", - ['Ậ'] = "A", - ['ậ'] = "a", - ['Ắ'] = "A", - ['ắ'] = "a", - ['Ằ'] = "A", - ['ằ'] = "a", - ['Ẳ'] = "A", - ['ẳ'] = "a", - ['Ẵ'] = "A", - ['ẵ'] = "a", - ['Ặ'] = "A", - ['ặ'] = "a", - ['Ẹ'] = "E", - ['ẹ'] = "e", - ['Ẻ'] = "E", - ['ẻ'] = "e", - ['Ẽ'] = "E", - ['ẽ'] = "e", - ['Ế'] = "E", - ['ế'] = "e", - ['Ề'] = "E", - ['ề'] = "e", - ['Ể'] = "E", - ['ể'] = "e", - ['Ễ'] = "E", - ['ễ'] = "e", - ['Ệ'] = "E", - ['ệ'] = "e", - ['Ỉ'] = "I", - ['ỉ'] = "i", - ['Ị'] = "I", - ['ị'] = "i", - ['Ọ'] = "O", - ['ọ'] = "o", - ['Ỏ'] = "O", - ['ỏ'] = "o", - ['Ố'] = "O", - ['ố'] = "o", - ['Ồ'] = "O", - ['ồ'] = "o", - ['Ổ'] = "O", - ['ổ'] = "o", - ['Ỗ'] = "O", - ['ỗ'] = "o", - ['Ộ'] = "O", - ['ộ'] = "o", - ['Ớ'] = "O", - ['ớ'] = "o", - ['Ờ'] = "O", - ['ờ'] = "o", - ['Ở'] = "O", - ['ở'] = "o", - ['Ỡ'] = "O", - ['ỡ'] = "o", - ['Ợ'] = "O", - ['ợ'] = "o", - ['Ụ'] = "U", - ['ụ'] = "u", - ['Ủ'] = "U", - ['ủ'] = "u", - ['Ứ'] = "U", - ['ứ'] = "u", - ['Ừ'] = "U", - ['ừ'] = "u", - ['Ử'] = "U", - ['ử'] = "u", - ['Ữ'] = "U", - ['ữ'] = "u", - ['Ự'] = "U", - ['ự'] = "u", - ['Ỳ'] = "Y", - ['ỳ'] = "y", - ['Ỵ'] = "Y", - ['ỵ'] = "y", - ['Ỷ'] = "Y", - ['ỷ'] = "y", - ['Ỹ'] = "Y", - ['ỹ'] = "y", - ['‘'] = "\'", - ['’'] = "\'", - ['“'] = "\\\"", - ['”'] = "\\\"", - ['†'] = "+", - ['•'] = "*", - ['…'] = "...", - ['₠'] = "ecu", - ['₢'] = "cruzeiro", - ['₣'] = "french franc", - ['₤'] = "lira", - ['₥'] = "mill", - ['₦'] = "naira", - ['₧'] = "peseta", - ['₨'] = "rupee", - ['₩'] = "won", - ['₪'] = "new shequel", - ['₫'] = "dong", - ['€'] = "euro", - ['₭'] = "kip", - ['₮'] = "tugrik", - ['₯'] = "drachma", - ['₰'] = "penny", - ['₱'] = "peso", - ['₲'] = "guarani", - ['₳'] = "austral", - ['₴'] = "hryvnia", - ['₵'] = "cedi", - ['₹'] = "indian rupee", - ['₽'] = "russian ruble", - ['₿'] = "bitcoin", - ['℠'] = "sm", - ['™'] = "tm", - ['∂'] = "d", - ['∆'] = "delta", - ['∑'] = "sum", - ['∞'] = "infinity", - ['♥'] = "love", - ['元'] = "yuan", - ['円'] = "yen", - ['﷼'] = "rial" - }; - - static StringExtensions() - { - LowerCaseDiacritics = Diacritics.ToDictionary(x => x.Key, x => x.Value.ToLowerInvariant()); - } - - public static bool IsSlug(this string value) - { - return value != null && SlugRegex.IsMatch(value); - } - - public static bool IsEmail(this string value) - { - return value != null && EmailRegex.IsMatch(value); - } - - public static bool IsPropertyName(this string value) - { - return value != null && PropertyNameRegex.IsMatch(value); - } - - public static string WithFallback(this string value, string fallback) - { - return !string.IsNullOrWhiteSpace(value) ? value.Trim() : fallback; - } - - public static string ToPascalCase(this string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return string.Empty; - } - - var sb = new StringBuilder(value.Length); - - var last = NullChar; - var length = 0; - - for (var i = 0; i < value.Length; i++) - { - var c = value[i]; - - if (c == '-' || c == '_' || c == ' ') - { - if (last != NullChar) - { - sb.Append(char.ToUpperInvariant(last)); - } - - last = NullChar; - length = 0; - } - else - { - if (length > 1) - { - sb.Append(c); - } - else if (length == 0) - { - last = c; - } - else - { - sb.Append(char.ToUpperInvariant(last)); - sb.Append(c); - - last = NullChar; - } - - length++; - } - } - - if (last != NullChar) - { - sb.Append(char.ToUpperInvariant(last)); - } - - return sb.ToString(); - } - - public static string ToKebabCase(this string value) - { - if (value.Length == 0) - { - return string.Empty; - } - - var sb = new StringBuilder(value.Length); - - var length = 0; - - for (var i = 0; i < value.Length; i++) - { - var c = value[i]; - - if (c == '-' || c == '_' || c == ' ') - { - length = 0; - } - else - { - if (length > 0) - { - sb.Append(char.ToLowerInvariant(c)); - } - else - { - if (sb.Length > 0) - { - sb.Append('-'); - } - - sb.Append(char.ToLowerInvariant(c)); - } - - length++; - } - } - - return sb.ToString(); - } - - public static string ToCamelCase(this string value) - { - if (value.Length == 0) - { - return string.Empty; - } - - var sb = new StringBuilder(value.Length); - - var last = NullChar; - var length = 0; - - for (var i = 0; i < value.Length; i++) - { - var c = value[i]; - - if (c == '-' || c == '_' || c == ' ') - { - if (last != NullChar) - { - if (sb.Length > 0) - { - sb.Append(char.ToUpperInvariant(last)); - } - else - { - sb.Append(char.ToLowerInvariant(last)); - } - } - - last = NullChar; - length = 0; - } - else - { - if (length > 1) - { - sb.Append(c); - } - else if (length == 0) - { - last = c; - } - else - { - if (sb.Length > 0) - { - sb.Append(char.ToUpperInvariant(last)); - } - else - { - sb.Append(char.ToLowerInvariant(last)); - } - - sb.Append(c); - - last = NullChar; - } - - length++; - } - } - - if (last != NullChar) - { - if (sb.Length > 0) - { - sb.Append(char.ToUpperInvariant(last)); - } - else - { - sb.Append(char.ToLowerInvariant(last)); - } - } - - return sb.ToString(); - } - - public static string Slugify(this string value, ISet preserveHash = null, bool singleCharDiactric = false, char separator = '-') - { - var result = new StringBuilder(value.Length); - - var lastChar = (char)0; - - for (var i = 0; i < value.Length; i++) - { - var character = value[i]; - - if (preserveHash?.Contains(character) == true) - { - result.Append(character); - } - else if (char.IsLetter(character) || char.IsNumber(character)) - { - lastChar = character; - - var lower = char.ToLowerInvariant(character); - - if (LowerCaseDiacritics.TryGetValue(character, out var replacement)) - { - if (singleCharDiactric && replacement.Length == 2) - { - result.Append(replacement[0]); - } - else - { - result.Append(replacement); - } - } - else - { - result.Append(lower); - } - } - else if ((i < value.Length - 1) && (i > 0 && lastChar != separator)) - { - lastChar = separator; - - result.Append(separator); - } - } - - return result.ToString().Trim(separator); - } - - public static string BuildFullUrl(this string baseUrl, string path, bool trailingSlash = false) - { - Guard.NotNull(path, nameof(path)); - - var url = $"{baseUrl.TrimEnd('/')}/{path.Trim('/')}"; - - if (trailingSlash && - url.IndexOf("#", StringComparison.OrdinalIgnoreCase) < 0 && - url.IndexOf("?", StringComparison.OrdinalIgnoreCase) < 0 && - url.IndexOf(";", StringComparison.OrdinalIgnoreCase) < 0) - { - url += "/"; - } - - return url; - } - - public static string JoinNonEmpty(string separator, params string[] parts) - { - Guard.NotNull(separator, nameof(separator)); - - if (parts == null || parts.Length == 0) - { - return string.Empty; - } - - return string.Join(separator, parts.Where(x => !string.IsNullOrWhiteSpace(x))); - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs b/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs deleted file mode 100644 index a9f2b8cac..000000000 --- a/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; - -namespace Squidex.Infrastructure.Tasks -{ - public sealed class AsyncLocalCleaner : IDisposable - { - private readonly AsyncLocal asyncLocal; - - public AsyncLocalCleaner(AsyncLocal asyncLocal) - { - Guard.NotNull(asyncLocal, nameof(asyncLocal)); - - this.asyncLocal = asyncLocal; - } - - public void Dispose() - { - asyncLocal.Value = default; - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/AsyncLock.cs b/src/Squidex.Infrastructure/Tasks/AsyncLock.cs deleted file mode 100644 index 60e11e804..000000000 --- a/src/Squidex.Infrastructure/Tasks/AsyncLock.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; - -#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body - -namespace Squidex.Infrastructure.Tasks -{ - public sealed class AsyncLock - { - private readonly SemaphoreSlim semaphore; - - public AsyncLock() - { - semaphore = new SemaphoreSlim(1); - } - - public Task LockAsync() - { - var wait = semaphore.WaitAsync(); - - if (wait.IsCompleted) - { - return Task.FromResult((IDisposable)new LockReleaser(this)); - } - else - { - return wait.ContinueWith(x => (IDisposable)new LockReleaser(this), - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } - } - - private class LockReleaser : IDisposable - { - private AsyncLock target; - - internal LockReleaser(AsyncLock target) - { - this.target = target; - } - - public void Dispose() - { - var current = target; - - if (current == null) - { - return; - } - - target = null; - - try - { - current.semaphore.Release(); - } - catch - { - // just ignore the Exception - } - } - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs b/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs deleted file mode 100644 index 6c8e99ba2..000000000 --- a/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Tasks -{ - public sealed class AsyncLockPool - { - private readonly AsyncLock[] locks; - - public AsyncLockPool(int poolSize) - { - Guard.GreaterThan(poolSize, 0, nameof(poolSize)); - - locks = new AsyncLock[poolSize]; - - for (var i = 0; i < poolSize; i++) - { - locks[i] = new AsyncLock(); - } - } - - public Task LockAsync(object target) - { - Guard.NotNull(target, nameof(target)); - - return locks[Math.Abs(target.GetHashCode() % locks.Length)].LockAsync(); - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs b/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs deleted file mode 100644 index 2f61604c8..000000000 --- a/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Infrastructure.Tasks -{ - public class PartitionedActionBlock : ITargetBlock - { - private readonly ITargetBlock distributor; - private readonly ActionBlock[] workers; - - public Task Completion - { - get { return Task.WhenAll(workers.Select(x => x.Completion)); } - } - - public PartitionedActionBlock(Action action, Func partitioner) - : this (action?.ToAsync(), partitioner, new ExecutionDataflowBlockOptions()) - { - } - - public PartitionedActionBlock(Func action, Func partitioner) - : this(action, partitioner, new ExecutionDataflowBlockOptions()) - { - } - - public PartitionedActionBlock(Action action, Func partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) - : this(action?.ToAsync(), partitioner, dataflowBlockOptions) - { - } - - public PartitionedActionBlock(Func action, Func partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) - { - Guard.NotNull(action, nameof(action)); - Guard.NotNull(partitioner, nameof(partitioner)); - Guard.NotNull(dataflowBlockOptions, nameof(dataflowBlockOptions)); - Guard.GreaterThan(dataflowBlockOptions.MaxDegreeOfParallelism, 1, nameof(dataflowBlockOptions.MaxDegreeOfParallelism)); - - workers = new ActionBlock[dataflowBlockOptions.MaxDegreeOfParallelism]; - - for (var i = 0; i < dataflowBlockOptions.MaxDegreeOfParallelism; i++) - { - var workerOption = SimpleMapper.Map(dataflowBlockOptions, new ExecutionDataflowBlockOptions()); - - workerOption.MaxDegreeOfParallelism = 1; - workerOption.MaxMessagesPerTask = 1; - - workers[i] = new ActionBlock(action, workerOption); - } - - var distributorOption = new ExecutionDataflowBlockOptions - { - MaxDegreeOfParallelism = 1, - MaxMessagesPerTask = 1, - BoundedCapacity = 1 - }; - - distributor = new ActionBlock(x => - { - var partition = Math.Abs(partitioner(x)) % workers.Length; - - return workers[partition].SendAsync(x); - }, distributorOption); - - distributor.Completion.ContinueWith(x => - { - foreach (var worker in workers) - { - worker.Complete(); - } - }); - } - - public DataflowMessageStatus OfferMessage(DataflowMessageHeader messageHeader, TInput messageValue, ISourceBlock source, bool consumeToAccept) - { - return distributor.OfferMessage(messageHeader, messageValue, source, consumeToAccept); - } - - public void Complete() - { - distributor.Complete(); - } - - public void Fault(Exception exception) - { - distributor.Fault(exception); - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs b/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs deleted file mode 100644 index f56f1ac46..000000000 --- a/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; - -namespace Squidex.Infrastructure.Tasks -{ - public sealed class SingleThreadedDispatcher - { - private readonly ActionBlock> block; - private bool isStopped; - - public SingleThreadedDispatcher(int capacity = 1) - { - var options = new ExecutionDataflowBlockOptions - { - BoundedCapacity = capacity, - MaxMessagesPerTask = 1, - MaxDegreeOfParallelism = 1 - }; - - block = new ActionBlock>(Handle, options); - } - - public Task DispatchAsync(Func action) - { - Guard.NotNull(action, nameof(action)); - - return block.SendAsync(action); - } - - public Task DispatchAsync(Action action) - { - Guard.NotNull(action, nameof(action)); - - return block.SendAsync(() => { action(); return TaskHelper.Done; }); - } - - public async Task StopAndWaitAsync() - { - await DispatchAsync(() => - { - isStopped = true; - - block.Complete(); - }); - - await block.Completion; - } - - private Task Handle(Func action) - { - if (isStopped) - { - return TaskHelper.Done; - } - - return action(); - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs b/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs deleted file mode 100644 index 6f7bf787c..000000000 --- a/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Tasks -{ - public static class TaskExtensions - { - private static readonly Action IgnoreTaskContinuation = t => { var ignored = t.Exception; }; - - public static void Forget(this Task task) - { - if (task.IsCompleted) - { - var ignored = task.Exception; - } - else - { - task.ContinueWith( - IgnoreTaskContinuation, - CancellationToken.None, - TaskContinuationOptions.OnlyOnFaulted | - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } - } - - public static Func ToDefault(this Action action) - { - Guard.NotNull(action, nameof(action)); - - return x => - { - action(x); - - return default; - }; - } - - public static Func> ToDefault(this Func action) - { - Guard.NotNull(action, nameof(action)); - - return async x => - { - await action(x); - - return default; - }; - } - - public static Func> ToAsync(this Func action) - { - Guard.NotNull(action, nameof(action)); - - return x => - { - var result = action(x); - - return Task.FromResult(result); - }; - } - - public static Func ToAsync(this Action action) - { - return x => - { - action(x); - - return TaskHelper.Done; - }; - } - - public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - using (cancellationToken.Register(state => - { - ((TaskCompletionSource)state).TrySetResult(null); - }, - tcs)) - { - var resultTask = await Task.WhenAny(task, tcs.Task); - if (resultTask == tcs.Task) - { - throw new OperationCanceledException(cancellationToken); - } - - return await task; - } - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/TaskHelper.cs b/src/Squidex.Infrastructure/Tasks/TaskHelper.cs deleted file mode 100644 index 6c59a1d37..000000000 --- a/src/Squidex.Infrastructure/Tasks/TaskHelper.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Tasks -{ - public static class TaskHelper - { - public static readonly Task Done = CreateDoneTask(); - public static readonly Task False = CreateResultTask(false); - public static readonly Task True = CreateResultTask(true); - - private static Task CreateDoneTask() - { - var result = new TaskCompletionSource(); - - result.SetResult(null); - - return result.Task; - } - - private static Task CreateResultTask(bool value) - { - var result = new TaskCompletionSource(); - - result.SetResult(value); - - return result.Task; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Timers/CompletionTimer.cs b/src/Squidex.Infrastructure/Timers/CompletionTimer.cs deleted file mode 100644 index a61a136d0..000000000 --- a/src/Squidex.Infrastructure/Timers/CompletionTimer.cs +++ /dev/null @@ -1,89 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Timers -{ - public sealed class CompletionTimer - { - private const int OneCallNotExecuted = 0; - private const int OneCallExecuted = 1; - private const int OneCallRequested = 2; - private readonly CancellationTokenSource stopToken = new CancellationTokenSource(); - private readonly Task runTask; - private int oneCallState; - private CancellationTokenSource wakeupToken; - - public CompletionTimer(int delayInMs, Func callback, int initialDelay = 0) - { - Guard.NotNull(callback, nameof(callback)); - Guard.GreaterThan(delayInMs, 0, nameof(delayInMs)); - - runTask = RunInternalAsync(delayInMs, initialDelay, callback); - } - - public Task StopAsync() - { - stopToken.Cancel(); - - return runTask; - } - - public void SkipCurrentDelay() - { - if (!stopToken.IsCancellationRequested) - { - Interlocked.CompareExchange(ref oneCallState, OneCallRequested, OneCallNotExecuted); - - wakeupToken?.Cancel(); - } - } - - private async Task RunInternalAsync(int delay, int initialDelay, Func callback) - { - try - { - if (initialDelay > 0) - { - await WaitAsync(initialDelay).ConfigureAwait(false); - } - - while (oneCallState == OneCallRequested || !stopToken.IsCancellationRequested) - { - await callback(stopToken.Token).ConfigureAwait(false); - - oneCallState = OneCallExecuted; - - await WaitAsync(delay).ConfigureAwait(false); - } - } - catch - { - return; - } - } - - private async Task WaitAsync(int intervall) - { - try - { - wakeupToken = new CancellationTokenSource(); - - using (var cts = CancellationTokenSource.CreateLinkedTokenSource(stopToken.Token, wakeupToken.Token)) - { - await Task.Delay(intervall, cts.Token).ConfigureAwait(false); - } - } - catch (OperationCanceledException) - { - } - } - } -} diff --git a/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs b/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs deleted file mode 100644 index 5409faca1..000000000 --- a/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs +++ /dev/null @@ -1,95 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Squidex.Infrastructure.Json; - -namespace Squidex.Infrastructure.Translations -{ - public sealed class DeepLTranslator : ITranslator - { - private const string Url = "https://api.deepl.com/v2/translate"; - private readonly HttpClient httpClient = new HttpClient(); - private readonly DeepLTranslatorOptions deepLOptions; - private readonly IJsonSerializer jsonSerializer; - - private sealed class Response - { - public ResponseTranslation[] Translations { get; set; } - } - - private sealed class ResponseTranslation - { - public string Text { get; set; } - } - - public DeepLTranslator(IOptions deepLOptions, IJsonSerializer jsonSerializer) - { - Guard.NotNull(deepLOptions, nameof(deepLOptions)); - Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); - - this.deepLOptions = deepLOptions.Value; - - this.jsonSerializer = jsonSerializer; - } - - public async Task Translate(string sourceText, Language targetLanguage, Language sourceLanguage = null, CancellationToken ct = default) - { - if (string.IsNullOrWhiteSpace(sourceText) || targetLanguage == null) - { - return new Translation(TranslationResult.NotTranslated, sourceText); - } - - if (string.IsNullOrWhiteSpace(deepLOptions.AuthKey)) - { - return new Translation(TranslationResult.NotImplemented); - } - - var parameters = new Dictionary - { - ["auth_key"] = deepLOptions.AuthKey, - ["text"] = sourceText, - ["target_lang"] = GetLanguageCode(targetLanguage) - }; - - if (sourceLanguage != null) - { - parameters["source_lang"] = GetLanguageCode(sourceLanguage); - } - - var response = await httpClient.PostAsync(Url, new FormUrlEncodedContent(parameters), ct); - var responseString = await response.Content.ReadAsStringAsync(); - - if (response.IsSuccessStatusCode) - { - var result = jsonSerializer.Deserialize(responseString); - - if (result?.Translations?.Length == 1) - { - return new Translation(TranslationResult.Translated, result.Translations[0].Text); - } - } - - if (response.StatusCode == HttpStatusCode.BadRequest) - { - return new Translation(TranslationResult.LanguageNotSupported, resultText: responseString); - } - - return new Translation(TranslationResult.Failed, resultText: responseString); - } - - private static string GetLanguageCode(Language language) - { - return language.Iso2Code.Substring(0, 2).ToUpperInvariant(); - } - } -} diff --git a/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs b/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs deleted file mode 100644 index d7124e343..000000000 --- a/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Translations -{ - public sealed class DeepLTranslatorOptions - { - public string AuthKey { get; set; } - } -} diff --git a/src/Squidex.Infrastructure/Translations/ITranslator.cs b/src/Squidex.Infrastructure/Translations/ITranslator.cs deleted file mode 100644 index 456923bb4..000000000 --- a/src/Squidex.Infrastructure/Translations/ITranslator.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Translations -{ - public interface ITranslator - { - Task Translate(string sourceText, Language targetLanguage, Language sourceLanguage = null, CancellationToken ct = default); - } -} diff --git a/src/Squidex.Infrastructure/Translations/NoopTranslator.cs b/src/Squidex.Infrastructure/Translations/NoopTranslator.cs deleted file mode 100644 index e872675c9..000000000 --- a/src/Squidex.Infrastructure/Translations/NoopTranslator.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Translations -{ - public sealed class NoopTranslator : ITranslator - { - public Task Translate(string sourceText, Language targetLanguage, Language sourceLanguage = null, CancellationToken ct = default) - { - var result = new Translation(TranslationResult.NotImplemented); - - return Task.FromResult(result); - } - } -} diff --git a/src/Squidex.Infrastructure/Translations/Translation.cs b/src/Squidex.Infrastructure/Translations/Translation.cs deleted file mode 100644 index 7bcabad2a..000000000 --- a/src/Squidex.Infrastructure/Translations/Translation.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Translations -{ - public sealed class Translation - { - public TranslationResult Result { get; } - - public string Text { get; } - - public string ResultText { get; set; } - - public Translation(TranslationResult result, string text = null, string resultText = null) - { - Text = text; - Result = result; - ResultText = resultText; - } - } -} diff --git a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs deleted file mode 100644 index 70091fd1c..000000000 --- a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs +++ /dev/null @@ -1,198 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; -using Squidex.Infrastructure.Timers; - -namespace Squidex.Infrastructure.UsageTracking -{ - public sealed class BackgroundUsageTracker : DisposableObjectBase, IUsageTracker - { - public const string CounterTotalCalls = "TotalCalls"; - public const string CounterTotalElapsedMs = "TotalElapsedMs"; - - private const string FallbackCategory = "*"; - private const int Intervall = 60 * 1000; - private readonly IUsageRepository usageRepository; - private readonly ISemanticLog log; - private readonly CompletionTimer timer; - private ConcurrentDictionary<(string Key, string Category), Usage> usages = new ConcurrentDictionary<(string Key, string Category), Usage>(); - - public BackgroundUsageTracker(IUsageRepository usageRepository, ISemanticLog log) - { - Guard.NotNull(usageRepository, nameof(usageRepository)); - Guard.NotNull(log, nameof(log)); - - this.usageRepository = usageRepository; - - this.log = log; - - timer = new CompletionTimer(Intervall, ct => TrackAsync(), Intervall); - } - - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - timer.StopAsync().Wait(); - } - } - - public void Next() - { - ThrowIfDisposed(); - - timer.SkipCurrentDelay(); - } - - private async Task TrackAsync() - { - try - { - var today = DateTime.Today; - - var localUsages = Interlocked.Exchange(ref usages, new ConcurrentDictionary<(string Key, string Category), Usage>()); - - if (localUsages.Count > 0) - { - var updates = new UsageUpdate[localUsages.Count]; - var updateIndex = 0; - - foreach (var kvp in localUsages) - { - var counters = new Counters - { - [CounterTotalCalls] = kvp.Value.Count, - [CounterTotalElapsedMs] = kvp.Value.ElapsedMs - }; - - updates[updateIndex].Key = kvp.Key.Key; - updates[updateIndex].Category = kvp.Key.Category; - updates[updateIndex].Counters = counters; - updates[updateIndex].Date = today; - - updateIndex++; - } - - await usageRepository.TrackUsagesAsync(updates); - } - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "TrackUsage") - .WriteProperty("status", "Failed")); - } - } - - public Task TrackAsync(string key, string category, double weight, double elapsedMs) - { - key = GetKey(key); - - ThrowIfDisposed(); - - if (weight > 0) - { - category = GetCategory(category); - - usages.AddOrUpdate((key, category), _ => new Usage(elapsedMs, weight), (k, x) => x.Add(elapsedMs, weight)); - } - - return TaskHelper.Done; - } - - public async Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate) - { - key = GetKey(key); - - ThrowIfDisposed(); - - var usagesFlat = await usageRepository.QueryAsync(key, fromDate, toDate); - var usagesByCategory = usagesFlat.GroupBy(x => GetCategory(x.Category)).ToDictionary(x => x.Key, x => x.ToList()); - - var result = new Dictionary>(); - - IEnumerable categories = usagesByCategory.Keys; - - if (usagesByCategory.Count == 0) - { - var enriched = new List(); - - for (var date = fromDate; date <= toDate; date = date.AddDays(1)) - { - enriched.Add(new DateUsage(date, 0, 0)); - } - - result[FallbackCategory] = enriched; - } - else - { - foreach (var category in categories) - { - var enriched = new List(); - - var usagesDictionary = usagesByCategory[category].ToDictionary(x => x.Date); - - for (var date = fromDate; date <= toDate; date = date.AddDays(1)) - { - var stored = usagesDictionary.GetOrDefault(date); - - var totalCount = 0L; - var totalElapsedMs = 0L; - - if (stored != null) - { - totalCount = (long)stored.Counters.Get(CounterTotalCalls); - totalElapsedMs = (long)stored.Counters.Get(CounterTotalElapsedMs); - } - - enriched.Add(new DateUsage(date, totalCount, totalElapsedMs)); - } - - result[category] = enriched; - } - } - - return result; - } - - public Task GetMonthlyCallsAsync(string key, DateTime date) - { - return GetPreviousCallsAsync(key, new DateTime(date.Year, date.Month, 1), date); - } - - public async Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate) - { - key = GetKey(key); - - ThrowIfDisposed(); - - var originalUsages = await usageRepository.QueryAsync(key, fromDate, toDate); - - return originalUsages.Sum(x => (long)x.Counters.Get(CounterTotalCalls)); - } - - private static string GetCategory(string category) - { - return !string.IsNullOrWhiteSpace(category) ? category.Trim() : FallbackCategory; - } - - private static string GetKey(string key) - { - Guard.NotNull(key, nameof(key)); - - return $"{key}_API"; - } - } -} diff --git a/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs deleted file mode 100644 index 2ba8ec8a1..000000000 --- a/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs +++ /dev/null @@ -1,71 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; -using Squidex.Infrastructure.Caching; - -namespace Squidex.Infrastructure.UsageTracking -{ - public sealed class CachingUsageTracker : CachingProviderBase, IUsageTracker - { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); - private readonly IUsageTracker inner; - - public CachingUsageTracker(IUsageTracker inner, IMemoryCache cache) - : base(cache) - { - Guard.NotNull(inner, nameof(inner)); - - this.inner = inner; - } - - public Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate) - { - Guard.NotNull(key, nameof(key)); - - return inner.QueryAsync(key, fromDate, toDate); - } - - public Task TrackAsync(string key, string category, double weight, double elapsedMs) - { - Guard.NotNull(key, nameof(key)); - - return inner.TrackAsync(key, category, weight, elapsedMs); - } - - public Task GetMonthlyCallsAsync(string key, DateTime date) - { - Guard.NotNull(key, nameof(key)); - - var cacheKey = string.Join("$", "Usage", nameof(GetMonthlyCallsAsync), key, date); - - return Cache.GetOrCreateAsync(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheDuration; - - return inner.GetMonthlyCallsAsync(key, date); - }); - } - - public Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate) - { - Guard.NotNull(key, nameof(key)); - - var cacheKey = string.Join("$", "Usage", nameof(GetPreviousCallsAsync), key, fromDate, toDate); - - return Cache.GetOrCreateAsync(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheDuration; - - return inner.GetPreviousCallsAsync(key, fromDate, toDate); - }); - } - } -} diff --git a/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs deleted file mode 100644 index bb94d83ff..000000000 --- a/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.UsageTracking -{ - public interface IUsageTracker - { - Task TrackAsync(string key, string category, double weight, double elapsedMs); - - Task GetMonthlyCallsAsync(string key, DateTime date); - - Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate); - - Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate); - } -} diff --git a/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs b/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs deleted file mode 100644 index 6a84b4129..000000000 --- a/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.UsageTracking -{ - public sealed class StoredUsage - { - public string Category { get; } - - public DateTime Date { get; } - - public Counters Counters { get; } - - public StoredUsage(string category, DateTime date, Counters counters) - { - Guard.NotNull(counters, nameof(counters)); - - Category = category; - Counters = counters; - - Date = date; - } - } -} diff --git a/src/Squidex.Infrastructure/Validation/Validate.cs b/src/Squidex.Infrastructure/Validation/Validate.cs deleted file mode 100644 index 0b37d8a4a..000000000 --- a/src/Squidex.Infrastructure/Validation/Validate.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Validation -{ - public static class Validate - { - public static void It(Func message, Action action) - { - List errors = null; - - var addValidation = new AddValidation((m, p) => - { - if (errors == null) - { - errors = new List(); - } - - errors.Add(new ValidationError(m, p)); - }); - - action(addValidation); - - if (errors != null) - { - throw new ValidationException(message(), errors); - } - } - - public static async Task It(Func message, Func action) - { - List errors = null; - - var addValidation = new AddValidation((m, p) => - { - if (errors == null) - { - errors = new List(); - } - - errors.Add(new ValidationError(m, p)); - }); - - await action(addValidation); - - if (errors != null) - { - throw new ValidationException(message(), errors); - } - } - } - - public delegate void AddValidation(string message, params string[] propertyNames); -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Validation/ValidationError.cs b/src/Squidex.Infrastructure/Validation/ValidationError.cs deleted file mode 100644 index 21db20f6c..000000000 --- a/src/Squidex.Infrastructure/Validation/ValidationError.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Squidex.Infrastructure.Validation -{ - [Serializable] - public sealed class ValidationError - { - private readonly string message; - private readonly string[] propertyNames; - - public string Message - { - get { return message; } - } - - public IEnumerable PropertyNames - { - get { return propertyNames; } - } - - public ValidationError(string message, params string[] propertyNames) - { - Guard.NotNullOrEmpty(message, nameof(message)); - - this.message = message; - - this.propertyNames = propertyNames ?? Array.Empty(); - } - - public ValidationError WithPrefix(string prefix) - { - if (propertyNames.Length > 0) - { - return new ValidationError(Message, propertyNames.Select(x => $"{prefix}.{x}").ToArray()); - } - else - { - return new ValidationError(Message, prefix); - } - } - - public void AddTo(AddValidation e) - { - e(Message, propertyNames); - } - } -} diff --git a/src/Squidex.Infrastructure/Validation/ValidationException.cs b/src/Squidex.Infrastructure/Validation/ValidationException.cs deleted file mode 100644 index 3ad48a3c7..000000000 --- a/src/Squidex.Infrastructure/Validation/ValidationException.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; - -namespace Squidex.Infrastructure.Validation -{ - [Serializable] - public class ValidationException : DomainException - { - private static readonly List FallbackErrors = new List(); - private readonly IReadOnlyList errors; - - public IReadOnlyList Errors - { - get { return errors ?? FallbackErrors; } - } - - public string Summary { get; } - - public ValidationException(string summary, params ValidationError[] errors) - : this(summary, null, errors?.ToList()) - { - } - - public ValidationException(string summary, IReadOnlyList errors) - : this(summary, null, errors) - { - this.errors = errors ?? FallbackErrors; - } - - public ValidationException(string summary, Exception inner, params ValidationError[] errors) - : this(summary, inner, errors?.ToList()) - { - } - - public ValidationException(string summary, Exception inner, IReadOnlyList errors) - : base(FormatMessage(summary, errors), inner) - { - Summary = summary; - - this.errors = errors ?? FallbackErrors; - } - - protected ValidationException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - Summary = info.GetString(nameof(Summary)); - - errors = (List)info.GetValue(nameof(errors), typeof(List)); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(Summary), Summary); - info.AddValue(nameof(errors), errors.ToList()); - - base.GetObjectData(info, context); - } - - private static string FormatMessage(string summary, IReadOnlyList errors) - { - var sb = new StringBuilder(); - - sb.Append(summary.TrimEnd(' ', '.', ':')); - - if (errors?.Count > 0) - { - sb.Append(": "); - - for (var i = 0; i < errors.Count; i++) - { - var error = errors[i]?.Message; - - if (!string.IsNullOrWhiteSpace(error)) - { - sb.Append(error); - - if (!error.EndsWith(".", StringComparison.OrdinalIgnoreCase)) - { - sb.Append("."); - } - - if (i < errors.Count - 1) - { - sb.Append(" "); - } - } - } - } - else - { - sb.Append("."); - } - - return sb.ToString(); - } - } -} diff --git a/src/Squidex.Shared/Permissions.cs b/src/Squidex.Shared/Permissions.cs deleted file mode 100644 index 1d7e6bdd0..000000000 --- a/src/Squidex.Shared/Permissions.cs +++ /dev/null @@ -1,184 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; - -namespace Squidex.Shared -{ - public static class Permissions - { - private static readonly List ForAppsNonSchemaList = new List(); - private static readonly List ForAppsSchemaList = new List(); - - public static IReadOnlyList ForAppsNonSchema - { - get { return ForAppsNonSchemaList; } - } - - public static IReadOnlyList ForAppsSchema - { - get { return ForAppsSchemaList; } - } - - public const string All = "squidex.*"; - - public const string Admin = "squidex.admin.*"; - public const string AdminOrleans = "squidex.admin.orleans"; - - public const string AdminAppCreate = "squidex.admin.apps.create"; - - public const string AdminRestore = "squidex.admin.restore"; - - public const string AdminEvents = "squidex.admin.events"; - public const string AdminEventsRead = "squidex.admin.events.read"; - public const string AdminEventsManage = "squidex.admin.events.manage"; - - public const string AdminUsers = "squidex.admin.users"; - public const string AdminUsersRead = "squidex.admin.users.read"; - public const string AdminUsersCreate = "squidex.admin.users.create"; - public const string AdminUsersUpdate = "squidex.admin.users.update"; - public const string AdminUsersUnlock = "squidex.admin.users.unlock"; - public const string AdminUsersLock = "squidex.admin.users.lock"; - - public const string App = "squidex.apps.{app}"; - public const string AppCommon = "squidex.apps.{app}.common"; - - public const string AppDelete = "squidex.apps.{app}.delete"; - public const string AppUpdate = "squidex.apps.{app}.update"; - public const string AppUpdateImage = "squidex.apps.{app}.update"; - public const string AppUpdateGeneral = "squidex.apps.{app}.general"; - - public const string AppClients = "squidex.apps.{app}.clients"; - public const string AppClientsRead = "squidex.apps.{app}.clients.read"; - public const string AppClientsCreate = "squidex.apps.{app}.clients.create"; - public const string AppClientsUpdate = "squidex.apps.{app}.clients.update"; - public const string AppClientsDelete = "squidex.apps.{app}.clients.delete"; - - public const string AppContributors = "squidex.apps.{app}.contributors"; - public const string AppContributorsRead = "squidex.apps.{app}.contributors.read"; - public const string AppContributorsAssign = "squidex.apps.{app}.contributors.assign"; - public const string AppContributorsRevoke = "squidex.apps.{app}.contributors.revoke"; - - public const string AppLanguages = "squidex.apps.{app}.languages"; - public const string AppLanguagesCreate = "squidex.apps.{app}.languages.create"; - public const string AppLanguagesUpdate = "squidex.apps.{app}.languages.update"; - public const string AppLanguagesDelete = "squidex.apps.{app}.languages.delete"; - - public const string AppRoles = "squidex.apps.{app}.roles"; - public const string AppRolesRead = "squidex.apps.{app}.roles.read"; - public const string AppRolesCreate = "squidex.apps.{app}.roles.create"; - public const string AppRolesUpdate = "squidex.apps.{app}.roles.update"; - public const string AppRolesDelete = "squidex.apps.{app}.roles.delete"; - - public const string AppPatterns = "squidex.apps.{app}.patterns"; - public const string AppPatternsCreate = "squidex.apps.{app}.patterns.create"; - public const string AppPatternsUpdate = "squidex.apps.{app}.patterns.update"; - public const string AppPatternsDelete = "squidex.apps.{app}.patterns.delete"; - - public const string AppWorkflows = "squidex.apps.{app}.workflows"; - public const string AppWorkflowsRead = "squidex.apps.{app}.workflows.read"; - public const string AppWorkflowsCreate = "squidex.apps.{app}.workflows.create"; - public const string AppWorkflowsUpdate = "squidex.apps.{app}.workflows.update"; - public const string AppWorkflowsDelete = "squidex.apps.{app}.workflows.delete"; - - public const string AppBackups = "squidex.apps.{app}.backups"; - public const string AppBackupsRead = "squidex.apps.{app}.backups.read"; - public const string AppBackupsCreate = "squidex.apps.{app}.backups.create"; - public const string AppBackupsDelete = "squidex.apps.{app}.backups.delete"; - - public const string AppPlans = "squidex.apps.{app}.plans"; - public const string AppPlansRead = "squidex.apps.{app}.plans.read"; - public const string AppPlansChange = "squidex.apps.{app}.plans.change"; - - public const string AppAssets = "squidex.apps.{app}.assets"; - public const string AppAssetsRead = "squidex.apps.{app}.assets.read"; - public const string AppAssetsCreate = "squidex.apps.{app}.assets.create"; - public const string AppAssetsUpdate = "squidex.apps.{app}.assets.update"; - public const string AppAssetsDelete = "squidex.apps.{app}.assets.delete"; - - public const string AppRules = "squidex.apps.{app}.rules"; - public const string AppRulesRead = "squidex.apps.{app}.rules.read"; - public const string AppRulesEvents = "squidex.apps.{app}.rules.events"; - public const string AppRulesCreate = "squidex.apps.{app}.rules.create"; - public const string AppRulesUpdate = "squidex.apps.{app}.rules.update"; - public const string AppRulesDisable = "squidex.apps.{app}.rules.disable"; - public const string AppRulesDelete = "squidex.apps.{app}.rules.delete"; - - public const string AppSchemas = "squidex.apps.{app}.schemas.{name}"; - public const string AppSchemasCreate = "squidex.apps.{app}.schemas.{name}.create"; - public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{name}.update"; - public const string AppSchemasScripts = "squidex.apps.{app}.schemas.{name}.scripts"; - public const string AppSchemasPublish = "squidex.apps.{app}.schemas.{name}.publish"; - public const string AppSchemasDelete = "squidex.apps.{app}.schemas.{name}.delete"; - - public const string AppContents = "squidex.apps.{app}.contents.{name}"; - public const string AppContentsRead = "squidex.apps.{app}.contents.{name}.read"; - public const string AppContentsCreate = "squidex.apps.{app}.contents.{name}.create"; - public const string AppContentsUpdate = "squidex.apps.{app}.contents.{name}.update"; - public const string AppContentsDraftDiscard = "squidex.apps.{app}.contents.{name}.draft.discard"; - public const string AppContentsDraftPublish = "squidex.apps.{app}.contents.{name}.draft.publish"; - public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete"; - - public const string AppApi = "squidex.apps.{app}.api"; - - static Permissions() - { - foreach (var field in typeof(Permissions).GetFields(BindingFlags.Public | BindingFlags.Static)) - { - if (field.IsLiteral && !field.IsInitOnly) - { - var value = (string)field.GetValue(null); - - if (value.StartsWith(App, StringComparison.OrdinalIgnoreCase)) - { - if (value.IndexOf("{name}", App.Length, StringComparison.OrdinalIgnoreCase) >= 0) - { - ForAppsSchemaList.Add(value); - } - else - { - ForAppsNonSchemaList.Add(value); - } - } - } - } - } - - public static Permission ForApp(string id, string app = Permission.Any, string schema = Permission.Any) - { - Guard.NotNull(id, nameof(id)); - - return new Permission(id.Replace("{app}", app ?? Permission.Any).Replace("{name}", schema ?? Permission.Any)); - } - - public static PermissionSet ToAppPermissions(this PermissionSet permissions, string app) - { - var matching = permissions.Where(x => x.StartsWith($"squidex.apps.{app}")); - - return new PermissionSet(matching); - } - - public static string[] ToAppNames(this PermissionSet permissions) - { - var matching = permissions.Where(x => x.StartsWith("squidex.apps.")); - - var result = - matching - .Select(x => x.Id.Split('.')).Where(x => x.Length > 2) - .Select(x => x[2]) - .Distinct() - .ToArray(); - - return result; - } - } -} diff --git a/src/Squidex.Shared/Squidex.Shared.csproj b/src/Squidex.Shared/Squidex.Shared.csproj deleted file mode 100644 index 51c30aaf0..000000000 --- a/src/Squidex.Shared/Squidex.Shared.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - ..\..\Squidex.ruleset - - - - - - - - \ No newline at end of file diff --git a/src/Squidex.Shared/Users/ClientUser.cs b/src/Squidex.Shared/Users/ClientUser.cs deleted file mode 100644 index 88f840eff..000000000 --- a/src/Squidex.Shared/Users/ClientUser.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Security.Claims; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Shared.Identity; - -namespace Squidex.Shared.Users -{ - public sealed class ClientUser : IUser - { - private readonly RefToken token; - private readonly List claims; - - public ClientUser(RefToken token) - { - Guard.NotNull(token, nameof(token)); - - this.token = token; - - claims = new List - { - new Claim(OpenIdClaims.ClientId, token.Identifier), - new Claim(SquidexClaimTypes.DisplayName, token.ToString()) - }; - } - - public string Id - { - get { return token.Identifier; } - } - - public string Email - { - get { return token.ToString(); } - } - - public bool IsLocked - { - get { return false; } - } - - public IReadOnlyList Claims - { - get { return claims; } - } - } -} diff --git a/src/Squidex.Shared/Users/IUserResolver.cs b/src/Squidex.Shared/Users/IUserResolver.cs deleted file mode 100644 index 6a3e8f1df..000000000 --- a/src/Squidex.Shared/Users/IUserResolver.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Shared.Users -{ - public interface IUserResolver - { - Task CreateUserIfNotExists(string email, bool invited = false); - - Task FindByIdOrEmailAsync(string idOrEmail); - - Task> QueryByEmailAsync(string email); - - Task> QueryManyAsync(string[] ids); - } -} diff --git a/src/Squidex.Shared/Users/UserExtensions.cs b/src/Squidex.Shared/Users/UserExtensions.cs deleted file mode 100644 index cc9adfd88..000000000 --- a/src/Squidex.Shared/Users/UserExtensions.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using Squidex.Infrastructure.Security; -using Squidex.Shared.Identity; - -namespace Squidex.Shared.Users -{ - public static class UserExtensions - { - public static PermissionSet Permissions(this IUser user) - { - return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x))); - } - - public static bool IsInvited(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.Invited, "true"); - } - - public static bool IsHidden(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.Hidden, "true"); - } - - public static bool HasConsent(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.Consent, "true"); - } - - public static bool HasConsentForEmails(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.ConsentForEmails, "true"); - } - - public static bool HasDisplayName(this IUser user) - { - return user.HasClaim(SquidexClaimTypes.DisplayName); - } - - public static bool HasPictureUrl(this IUser user) - { - return user.HasClaim(SquidexClaimTypes.PictureUrl); - } - - public static bool IsPictureUrlStored(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.PictureUrl, SquidexClaimTypes.PictureUrlStore); - } - - public static string ClientSecret(this IUser user) - { - return user.GetClaimValue(SquidexClaimTypes.ClientSecret); - } - - public static string PictureUrl(this IUser user) - { - return user.GetClaimValue(SquidexClaimTypes.PictureUrl); - } - - public static string DisplayName(this IUser user) - { - return user.GetClaimValue(SquidexClaimTypes.DisplayName); - } - - public static string GetClaimValue(this IUser user, string type) - { - return user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase))?.Value; - } - - public static string[] GetClaimValues(this IUser user, string type) - { - return user.Claims.Where(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)).Select(x => x.Value).ToArray(); - } - - public static bool HasClaim(this IUser user, string type) - { - return user.Claims.Any(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); - } - - public static bool HasClaimValue(this IUser user, string type, string value) - { - return user.Claims.Any(x => - string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase) && - string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase)); - } - - public static string PictureNormalizedUrl(this IUser user) - { - var url = user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.PictureUrl)?.Value; - - if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar")) - { - if (url.Contains("?")) - { - url += "&d=404"; - } - else - { - url += "?d=404"; - } - } - - return url; - } - } -} diff --git a/src/Squidex.Web/ApiController.cs b/src/Squidex.Web/ApiController.cs deleted file mode 100644 index a7ead50d9..000000000 --- a/src/Squidex.Web/ApiController.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Web -{ - [Area("Api")] - [ApiController] - [ApiExceptionFilter] - [ApiModelValidation(false)] - public abstract class ApiController : Controller - { - protected ICommandBus CommandBus { get; } - - protected IAppEntity App - { - get - { - var app = HttpContext.Context().App; - - if (app == null) - { - throw new InvalidOperationException("Not in a app context."); - } - - return app; - } - } - - protected Context Context - { - get { return HttpContext.Context(); } - } - - protected Guid AppId - { - get { return App.Id; } - } - - protected ApiController(ICommandBus commandBus) - { - Guard.NotNull(commandBus, nameof(commandBus)); - - CommandBus = commandBus; - } - - public override void OnActionExecuting(ActionExecutingContext context) - { - var request = context.HttpContext.Request; - - if (!request.PathBase.HasValue || !request.PathBase.Value.EndsWith("/api", StringComparison.OrdinalIgnoreCase)) - { - context.Result = new RedirectResult("/"); - } - } - } -} diff --git a/src/Squidex.Web/ApiExceptionFilterAttribute.cs b/src/Squidex.Web/ApiExceptionFilterAttribute.cs deleted file mode 100644 index 352ea93e0..000000000 --- a/src/Squidex.Web/ApiExceptionFilterAttribute.cs +++ /dev/null @@ -1,117 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security; -using System.Text; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Web -{ - public sealed class ApiExceptionFilterAttribute : ActionFilterAttribute, IExceptionFilter - { - private static readonly List> Handlers = new List>(); - - private static void AddHandler(Func handler) where T : Exception - { - Handlers.Add(ex => ex is T typed ? handler(typed) : null); - } - - static ApiExceptionFilterAttribute() - { - AddHandler(OnValidationException); - AddHandler(OnDecoderException); - AddHandler(OnDomainObjectNotFoundException); - AddHandler(OnDomainObjectVersionException); - AddHandler(OnDomainForbiddenException); - AddHandler(OnDomainException); - AddHandler(OnSecurityException); - } - - private static IActionResult OnDecoderException(DecoderFallbackException ex) - { - return ErrorResult(400, new ErrorDto { Message = ex.Message }); - } - - private static IActionResult OnDomainObjectNotFoundException(DomainObjectNotFoundException ex) - { - return new NotFoundResult(); - } - - private static IActionResult OnDomainObjectVersionException(DomainObjectVersionException ex) - { - return ErrorResult(412, new ErrorDto { Message = ex.Message }); - } - - private static IActionResult OnDomainException(DomainException ex) - { - return ErrorResult(400, new ErrorDto { Message = ex.Message }); - } - - private static IActionResult OnDomainForbiddenException(DomainForbiddenException ex) - { - return ErrorResult(403, new ErrorDto { Message = ex.Message }); - } - - private static IActionResult OnSecurityException(SecurityException ex) - { - return ErrorResult(403, new ErrorDto { Message = ex.Message }); - } - - private static IActionResult OnValidationException(ValidationException ex) - { - return ErrorResult(400, new ErrorDto { Message = ex.Summary, Details = ToDetails(ex) }); - } - - private static IActionResult ErrorResult(int statusCode, ErrorDto error) - { - error.StatusCode = statusCode; - - return new ObjectResult(error) { StatusCode = statusCode }; - } - - public void OnException(ExceptionContext context) - { - IActionResult result = null; - - foreach (var handler in Handlers) - { - result = handler(context.Exception); - - if (result != null) - { - break; - } - } - - if (result != null) - { - context.Result = result; - } - } - - private static string[] ToDetails(ValidationException ex) - { - return ex.Errors?.Select(e => - { - if (e.PropertyNames?.Any() == true) - { - return $"{string.Join(", ", e.PropertyNames)}: {e.Message}"; - } - else - { - return e.Message; - } - }).ToArray(); - } - } -} diff --git a/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs b/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs deleted file mode 100644 index 2b6aabf22..000000000 --- a/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Internal; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Squidex.Domain.Apps.Entities.Assets; - -namespace Squidex.Web -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - public sealed class AssetRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter - { - public int Order { get; set; } = 900; - - public bool IsReusable => true; - - public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) - { - var assetOptions = serviceProvider.GetService>(); - - if (assetOptions?.Value.MaxSize > 0) - { - var filter = serviceProvider.GetRequiredService(); - - filter.Bytes = assetOptions.Value.MaxSize; - - return filter; - } - else - { - var filter = serviceProvider.GetRequiredService(); - - return filter; - } - } - } -} diff --git a/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs b/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs deleted file mode 100644 index c837b5dd0..000000000 --- a/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Web.CommandMiddlewares -{ - public sealed class EnrichWithSchemaIdCommandMiddleware : ICommandMiddleware - { - private readonly IAppProvider appProvider; - private readonly IActionContextAccessor actionContextAccessor; - - public EnrichWithSchemaIdCommandMiddleware(IAppProvider appProvider, IActionContextAccessor actionContextAccessor) - { - this.appProvider = appProvider; - - this.actionContextAccessor = actionContextAccessor; - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (actionContextAccessor.ActionContext == null) - { - await next(); - - return; - } - - if (context.Command is ISchemaCommand schemaCommand && schemaCommand.SchemaId == null) - { - var schemaId = await GetSchemaIdAsync(context); - - schemaCommand.SchemaId = schemaId; - } - - if (context.Command is SchemaCommand schemaSelfCommand && schemaSelfCommand.SchemaId == Guid.Empty) - { - var schemaId = await GetSchemaIdAsync(context); - - schemaSelfCommand.SchemaId = schemaId?.Id ?? Guid.Empty; - } - - await next(); - } - - private async Task> GetSchemaIdAsync(CommandContext context) - { - NamedId appId = null; - - if (context.Command is IAppCommand appCommand) - { - appId = appCommand.AppId; - } - - if (appId == null) - { - appId = actionContextAccessor.ActionContext.HttpContext.Context().App?.NamedId(); - } - - if (appId != null) - { - var routeValues = actionContextAccessor.ActionContext.RouteData.Values; - - if (routeValues.ContainsKey("name")) - { - var schemaName = routeValues["name"].ToString(); - - ISchemaEntity schema; - - if (Guid.TryParse(schemaName, out var id)) - { - schema = await appProvider.GetSchemaAsync(appId.Id, id); - } - else - { - schema = await appProvider.GetSchemaAsync(appId.Id, schemaName); - } - - if (schema == null) - { - throw new DomainObjectNotFoundException(schemaName, typeof(ISchemaEntity)); - } - - return schema.NamedId(); - } - } - - return null; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Web/ContextProvider.cs b/src/Squidex.Web/ContextProvider.cs deleted file mode 100644 index 6d960b7eb..000000000 --- a/src/Squidex.Web/ContextProvider.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using Microsoft.AspNetCore.Http; -using Squidex.Domain.Apps.Entities; -using Squidex.Infrastructure; - -namespace Squidex.Web -{ - public sealed class ContextProvider : IContextProvider - { - private readonly IHttpContextAccessor httpContextAccessor; - private readonly AsyncLocal asyncLocal = new AsyncLocal(); - - public Context Context - { - get - { - if (httpContextAccessor.HttpContext == null) - { - if (asyncLocal.Value == null) - { - asyncLocal.Value = Context.Anonymous(); - } - - return asyncLocal.Value; - } - - return httpContextAccessor.HttpContext.Context(); - } - } - - public ContextProvider(IHttpContextAccessor httpContextAccessor) - { - Guard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); - - this.httpContextAccessor = httpContextAccessor; - } - } -} diff --git a/src/Squidex.Web/Deferred.cs b/src/Squidex.Web/Deferred.cs deleted file mode 100644 index 717182f49..000000000 --- a/src/Squidex.Web/Deferred.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure; - -namespace Squidex.Web -{ - public struct Deferred - { - private readonly Lazy> value; - - public Task Value - { - get { return value.Value; } - } - - private Deferred(Func> value) - { - this.value = new Lazy>(value); - } - - public static Deferred Response(Func factory) - { - Guard.NotNull(factory, nameof(factory)); - - return new Deferred(() => Task.FromResult(factory())); - } - - public static Deferred AsyncResponse(Func> factory) - { - Guard.NotNull(factory, nameof(factory)); - - return new Deferred(async () => await factory()); - } - } -} diff --git a/src/Squidex.Web/EntityCreatedDto.cs b/src/Squidex.Web/EntityCreatedDto.cs deleted file mode 100644 index 754f33f77..000000000 --- a/src/Squidex.Web/EntityCreatedDto.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.ComponentModel.DataAnnotations; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Web -{ - public sealed class EntityCreatedDto - { - [Required] - [Display(Description = "Id of the created entity.")] - public string Id { get; set; } - - [Display(Description = "The new version of the entity.")] - public long Version { get; set; } - - public static EntityCreatedDto FromResult(EntityCreatedResult result) - { - return new EntityCreatedDto { Id = result.IdOrValue?.ToString(), Version = result.Version }; - } - } -} diff --git a/src/Squidex.Web/ExposedValues.cs b/src/Squidex.Web/ExposedValues.cs deleted file mode 100644 index 4b56935cd..000000000 --- a/src/Squidex.Web/ExposedValues.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Reflection; -using System.Text; -using Microsoft.Extensions.Configuration; -using Squidex.Infrastructure; - -namespace Squidex.Web -{ - public sealed class ExposedValues : Dictionary - { - public ExposedValues() - { - } - - public ExposedValues(ExposedConfiguration configured, IConfiguration configuration, Assembly assembly = null) - { - Guard.NotNull(configured, nameof(configured)); - Guard.NotNull(configuration, nameof(configuration)); - - foreach (var kvp in configured) - { - var value = configuration.GetValue(kvp.Value); - - if (!string.IsNullOrWhiteSpace(value)) - { - this[kvp.Key] = value; - } - } - - if (assembly != null) - { - if (!ContainsKey("version")) - { - this["version"] = assembly.GetName().Version.ToString(); - } - } - } - - public override string ToString() - { - var sb = new StringBuilder(); - - foreach (var kvp in this) - { - if (sb.Length > 0) - { - sb.Append(", "); - } - - sb.Append(kvp.Key); - sb.Append(": "); - sb.Append(kvp.Value); - } - - return sb.ToString(); - } - } -} diff --git a/src/Squidex.Web/Extensions.cs b/src/Squidex.Web/Extensions.cs deleted file mode 100644 index d2c2fec97..000000000 --- a/src/Squidex.Web/Extensions.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Security.Claims; -using Microsoft.AspNetCore.Http; -using Squidex.Infrastructure.Security; - -namespace Squidex.Web -{ - public static class Extensions - { - public static string GetClientId(this ClaimsPrincipal principal) - { - var clientId = principal.FindFirst(OpenIdClaims.ClientId)?.Value; - - return clientId?.GetClientParts().ClientId; - } - - public static (string App, string ClientId) GetClientParts(this string clientId) - { - var parts = clientId.Split(':', '~'); - - if (parts.Length == 1) - { - return (null, parts[0]); - } - - if (parts.Length == 2) - { - return (parts[0], parts[1]); - } - - return (null, null); - } - - public static bool IsUser(this ApiController controller, string userId) - { - var subject = controller.User.OpenIdSubject(); - - return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase); - } - - public static bool TryGetHeaderString(this IHeaderDictionary headers, string header, out string result) - { - result = null; - - if (headers.TryGetValue(header, out var value)) - { - string valueString = value; - - if (!string.IsNullOrWhiteSpace(valueString)) - { - result = valueString; - return true; - } - } - - return false; - } - } -} diff --git a/src/Squidex.Web/FileCallbackResult.cs b/src/Squidex.Web/FileCallbackResult.cs deleted file mode 100644 index 5ca752eea..000000000 --- a/src/Squidex.Web/FileCallbackResult.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Web.Pipeline; - -namespace Squidex.Web -{ - public sealed class FileCallbackResult : FileResult - { - public bool Send404 { get; } - - public Func Callback { get; } - - public FileCallbackResult(string contentType, string name, bool send404, Func callback) - : base(contentType) - { - FileDownloadName = name; - - Send404 = send404; - - Callback = callback; - } - - public override Task ExecuteResultAsync(ActionContext context) - { - var executor = context.HttpContext.RequestServices.GetRequiredService(); - - return executor.ExecuteAsync(context, this); - } - } -} - -#pragma warning restore 1573 \ No newline at end of file diff --git a/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs b/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs deleted file mode 100644 index f87d632fd..000000000 --- a/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs +++ /dev/null @@ -1,95 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.Serialization; -using Newtonsoft.Json.Linq; -using NJsonSchema.Converters; -using Squidex.Infrastructure; - -#pragma warning disable RECS0108 // Warns about static fields in generic types - -namespace Squidex.Web.Json -{ - public class TypedJsonInheritanceConverter : JsonInheritanceConverter - { - private static readonly Lazy> DefaultMapping = new Lazy>(() => - { - var baseName = typeof(T).Name; - - var result = new Dictionary(); - - void AddType(Type type) - { - var discriminator = type.Name; - - if (discriminator.EndsWith(baseName, StringComparison.CurrentCulture)) - { - discriminator = discriminator.Substring(0, discriminator.Length - baseName.Length); - } - - result[discriminator] = type; - } - - foreach (var attribute in typeof(T).GetCustomAttributes()) - { - if (attribute.Type != null) - { - if (!attribute.Type.IsAbstract) - { - AddType(attribute.Type); - } - } - else if (!string.IsNullOrWhiteSpace(attribute.MethodName)) - { - var method = typeof(T).GetMethod(attribute.MethodName); - - if (method != null && method.IsStatic) - { - var types = (IEnumerable)method.Invoke(null, new object[0]); - - foreach (var type in types) - { - if (!type.IsAbstract) - { - AddType(type); - } - } - } - } - } - - return result; - }); - - private readonly IReadOnlyDictionary maping; - - public TypedJsonInheritanceConverter(string discriminator) - : this(discriminator, DefaultMapping.Value) - { - } - - public TypedJsonInheritanceConverter(string discriminator, IReadOnlyDictionary mapping) - : base(typeof(T), discriminator) - { - maping = mapping ?? DefaultMapping.Value; - } - - protected override Type GetDiscriminatorType(JObject jObject, Type objectType, string discriminatorValue) - { - return maping.GetOrDefault(discriminatorValue) ?? throw new InvalidOperationException($"Could not find subtype of '{objectType.Name}' with discriminator '{discriminatorValue}'."); - } - - public override string GetDiscriminatorValue(Type type) - { - return maping.FirstOrDefault(x => x.Value == type).Key ?? type.Name; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Web/PermissionExtensions.cs b/src/Squidex.Web/PermissionExtensions.cs deleted file mode 100644 index 68a2df38a..000000000 --- a/src/Squidex.Web/PermissionExtensions.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Http; -using Squidex.Infrastructure.Security; -using AllPermissions = Squidex.Shared.Permissions; - -namespace Squidex.Web -{ - public static class PermissionExtensions - { - public static PermissionSet Permissions(this HttpContext httpContext) - { - return httpContext.Context().Permissions; - } - - public static bool Includes(this HttpContext httpContext, Permission permission, PermissionSet additional = null) - { - return httpContext.Permissions().Includes(permission) || additional?.Includes(permission) == true; - } - - public static bool Includes(this ApiController controller, Permission permission, PermissionSet additional = null) - { - return controller.HttpContext.Includes(permission) || additional?.Includes(permission) == true; - } - - public static bool HasPermission(this HttpContext httpContext, Permission permission, PermissionSet additional = null) - { - return httpContext.Permissions().Allows(permission) || additional?.Allows(permission) == true; - } - - public static bool HasPermission(this ApiController controller, Permission permission, PermissionSet additional = null) - { - return controller.HttpContext.HasPermission(permission) || additional?.Allows(permission) == true; - } - - public static bool HasPermission(this ApiController controller, string id, string app = Permission.Any, string schema = Permission.Any, PermissionSet additional = null) - { - if (app == Permission.Any) - { - if (controller.RouteData.Values.TryGetValue("app", out var value) && value is string s) - { - app = s; - } - } - - if (schema == Permission.Any) - { - if (controller.RouteData.Values.TryGetValue("name", out var value) && value is string s) - { - schema = s; - } - } - - var permission = AllPermissions.ForApp(id, app, schema); - - return controller.HasPermission(permission, additional); - } - } -} diff --git a/src/Squidex.Web/Pipeline/ApiCostsFilter.cs b/src/Squidex.Web/Pipeline/ApiCostsFilter.cs deleted file mode 100644 index 9f1241dad..000000000 --- a/src/Squidex.Web/Pipeline/ApiCostsFilter.cs +++ /dev/null @@ -1,88 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.UsageTracking; - -namespace Squidex.Web.Pipeline -{ - public sealed class ApiCostsFilter : IAsyncActionFilter, IFilterContainer - { - private readonly IAppPlansProvider appPlansProvider; - private readonly IUsageTracker usageTracker; - - public ApiCostsFilter(IAppPlansProvider appPlansProvider, IUsageTracker usageTracker) - { - this.appPlansProvider = appPlansProvider; - - this.usageTracker = usageTracker; - } - - IFilterMetadata IFilterContainer.FilterDefinition { get; set; } - - public ApiCostsAttribute FilterDefinition - { - get - { - return (ApiCostsAttribute)((IFilterContainer)this).FilterDefinition; - } - set - { - ((IFilterContainer)this).FilterDefinition = value; - } - } - - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - context.HttpContext.Features.Set(FilterDefinition); - - var app = context.HttpContext.Context().App; - - if (app != null && FilterDefinition.Weight > 0) - { - var appId = app.Id.ToString(); - - using (Profiler.Trace("CheckUsage")) - { - var plan = appPlansProvider.GetPlanForApp(app); - - var usage = await usageTracker.GetMonthlyCallsAsync(appId, DateTime.Today); - - if (plan.MaxApiCalls >= 0 && usage > plan.MaxApiCalls * 1.1) - { - context.Result = new StatusCodeResult(429); - return; - } - } - - var watch = ValueStopwatch.StartNew(); - - try - { - await next(); - } - finally - { - var elapsedMs = watch.Stop(); - - await usageTracker.TrackAsync(appId, context.HttpContext.User.OpenIdClientId(), FilterDefinition.Weight, elapsedMs); - } - } - else - { - await next(); - } - } - } -} diff --git a/src/Squidex.Web/Pipeline/AppResolver.cs b/src/Squidex.Web/Pipeline/AppResolver.cs deleted file mode 100644 index 33782219b..000000000 --- a/src/Squidex.Web/Pipeline/AppResolver.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Authorization; -using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Infrastructure.Security; -using Squidex.Shared; -using Squidex.Shared.Identity; - -namespace Squidex.Web.Pipeline -{ - public sealed class AppResolver : IAsyncActionFilter - { - private readonly IAppProvider appProvider; - - public AppResolver(IAppProvider appProvider) - { - this.appProvider = appProvider; - } - - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - var user = context.HttpContext.User; - - var appName = context.RouteData.Values["app"]?.ToString(); - - if (!string.IsNullOrWhiteSpace(appName)) - { - var app = await appProvider.GetAppAsync(appName); - - if (app == null) - { - context.Result = new NotFoundResult(); - return; - } - - var (role, permissions) = FindByOpenIdSubject(app, user); - - if (permissions == null) - { - (role, permissions) = FindByOpenIdClient(app, user); - } - - if (permissions != null) - { - var identity = user.Identities.First(); - - if (!string.IsNullOrWhiteSpace(role)) - { - identity.AddClaim(new Claim(ClaimTypes.Role, role)); - } - - foreach (var permission in permissions) - { - identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission.Id)); - } - } - - var requestContext = context.HttpContext.Context(); - - requestContext.App = app; - requestContext.UpdatePermissions(); - - if (!requestContext.Permissions.Includes(Permissions.ForApp(Permissions.App, appName)) && !AllowAnonymous(context)) - { - context.Result = new NotFoundResult(); - return; - } - } - - await next(); - } - - private static bool AllowAnonymous(ActionExecutingContext context) - { - return context.ActionDescriptor.FilterDescriptors.Any(x => x.Filter is AllowAnonymousFilter); - } - - private static (string, PermissionSet) FindByOpenIdClient(IAppEntity app, ClaimsPrincipal user) - { - var clientId = user.GetClientId(); - - if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && app.Roles.TryGet(app.Name, client.Role, out var role)) - { - return (client.Role, role.Permissions); - } - - return (null, null); - } - - private static (string, PermissionSet) FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user) - { - var subjectId = user.OpenIdSubject(); - - if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var roleName) && app.Roles.TryGet(app.Name, roleName, out var role)) - { - return (roleName, role.Permissions); - } - - return (null, null); - } - } -} diff --git a/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs b/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs deleted file mode 100644 index b9f050191..000000000 --- a/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Caching; - -namespace Squidex.Web.Pipeline -{ - public sealed class LocalCacheMiddleware : IMiddleware - { - private readonly ILocalCache localCache; - - public LocalCacheMiddleware(ILocalCache localCache) - { - Guard.NotNull(localCache, nameof(localCache)); - - this.localCache = localCache; - } - - public async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - using (localCache.StartContext()) - { - await next(context); - } - } - } -} diff --git a/src/Squidex.Web/Resource.cs b/src/Squidex.Web/Resource.cs deleted file mode 100644 index 9719dfd1d..000000000 --- a/src/Squidex.Web/Resource.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using Newtonsoft.Json; -using Squidex.Infrastructure; - -namespace Squidex.Web -{ - public abstract class Resource - { - [JsonProperty("_links")] - [Required] - [Display(Description = "The links.")] - public Dictionary Links { get; } = new Dictionary(); - - public void AddSelfLink(string href) - { - AddGetLink("self", href); - } - - public void AddGetLink(string rel, string href, string metadata = null) - { - AddLink(rel, "GET", href, metadata); - } - - public void AddPatchLink(string rel, string href, string metadata = null) - { - AddLink(rel, "PATCH", href, metadata); - } - - public void AddPostLink(string rel, string href, string metadata = null) - { - AddLink(rel, "POST", href, metadata); - } - - public void AddPutLink(string rel, string href, string metadata = null) - { - AddLink(rel, "PUT", href, metadata); - } - - public void AddDeleteLink(string rel, string href, string metadata = null) - { - AddLink(rel, "DELETE", href, metadata); - } - - public void AddLink(string rel, string method, string href, string metadata = null) - { - Guard.NotNullOrEmpty(rel, nameof(rel)); - Guard.NotNullOrEmpty(href, nameof(href)); - Guard.NotNullOrEmpty(method, nameof(method)); - - Links[rel] = new ResourceLink { Href = href, Method = method, Metadata = metadata }; - } - } -} diff --git a/src/Squidex.Web/ResourceLink.cs b/src/Squidex.Web/ResourceLink.cs deleted file mode 100644 index d1caffc8d..000000000 --- a/src/Squidex.Web/ResourceLink.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.ComponentModel.DataAnnotations; - -namespace Squidex.Web -{ - public class ResourceLink - { - [Required] - [Display(Description = "The link url.")] - public string Href { get; set; } - - [Required] - [Display(Description = "The link method.")] - public string Method { get; set; } - - [Display(Description = "Additional data about the link.")] - public string Metadata { get; set; } - } -} diff --git a/src/Squidex.Web/Services/UrlGenerator.cs b/src/Squidex.Web/Services/UrlGenerator.cs deleted file mode 100644 index 5600beaaf..000000000 --- a/src/Squidex.Web/Services/UrlGenerator.cs +++ /dev/null @@ -1,78 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.Extensions.Options; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.GraphQL; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; - -namespace Squidex.Web.Services -{ - public sealed class UrlGenerator : IGraphQLUrlGenerator, IRuleUrlGenerator, IAssetUrlGenerator, IEmailUrlGenerator - { - private readonly IAssetStore assetStore; - private readonly UrlsOptions urlsOptions; - - public bool CanGenerateAssetSourceUrl { get; } - - public UrlGenerator(IOptions urlsOptions, IAssetStore assetStore, bool allowAssetSourceUrl) - { - this.assetStore = assetStore; - this.urlsOptions = urlsOptions.Value; - - CanGenerateAssetSourceUrl = allowAssetSourceUrl; - } - - public string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset) - { - if (!asset.IsImage) - { - return null; - } - - return urlsOptions.BuildUrl($"api/assets/{asset.Id}?version={asset.Version}&width=100&mode=Max"); - } - - public string GenerateUrl(string assetId) - { - return urlsOptions.BuildUrl($"api/assets/{assetId}"); - } - - public string GenerateAssetUrl(IAppEntity app, IAssetEntity asset) - { - return urlsOptions.BuildUrl($"api/assets/{asset.Id}?version={asset.Version}"); - } - - public string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content) - { - return urlsOptions.BuildUrl($"api/content/{app.Name}/{schema.SchemaDef.Name}/{content.Id}"); - } - - public string GenerateContentUIUrl(NamedId appId, NamedId schemaId, Guid contentId) - { - return urlsOptions.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}/{contentId}/history"); - } - - public string GenerateUIUrl() - { - return urlsOptions.BuildUrl("app/"); - } - - public string GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset) - { - return assetStore.GeneratePublicUrl(asset.Id.ToString(), asset.FileVersion, null); - } - } -} diff --git a/src/Squidex.Web/Squidex.Web.csproj b/src/Squidex.Web/Squidex.Web.csproj deleted file mode 100644 index 2c9404cb7..000000000 --- a/src/Squidex.Web/Squidex.Web.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Web/UrlHelperExtensions.cs b/src/Squidex.Web/UrlHelperExtensions.cs deleted file mode 100644 index 8d59cba5f..000000000 --- a/src/Squidex.Web/UrlHelperExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.AspNetCore.Mvc; - -#pragma warning disable RECS0108 // Warns about static fields in generic types - -namespace Squidex.Web -{ - public static class UrlHelperExtensions - { - private static class NameOf - { - public static readonly string Controller; - - static NameOf() - { - const string suffix = "Controller"; - - var name = typeof(T).Name; - - if (name.EndsWith(suffix, StringComparison.Ordinal)) - { - name = name.Substring(0, name.Length - suffix.Length); - } - - Controller = name; - } - } - - public static string Url(this IUrlHelper urlHelper, Func action, object values = null) where T : Controller - { - return urlHelper.Action(action(null), NameOf.Controller, values); - } - - public static string Url(this Controller controller, Func action, object values = null) where T : Controller - { - return controller.Url.Url(action, values); - } - } -} diff --git a/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs b/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs deleted file mode 100644 index 018fddad9..000000000 --- a/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Microsoft.Extensions.Options; -using NSwag; -using NSwag.Generation.Processors; -using NSwag.Generation.Processors.Contexts; -using Squidex.Web; - -namespace Squidex.Areas.Api.Config.OpenApi -{ - public sealed class CommonProcessor : IDocumentProcessor - { - private readonly string version; - private readonly string backgroundColor = "#3f83df"; - private readonly string logoUrl; - private readonly OpenApiExternalDocumentation documentation = new OpenApiExternalDocumentation - { - Url = "https://docs.squidex.io" - }; - - public CommonProcessor(ExposedValues exposedValues, IOptions urlOptions) - { - logoUrl = urlOptions.Value.BuildUrl("images/logo-white.png", false); - - if (!exposedValues.TryGetValue("version", out version)) - { - version = "1.0"; - } - } - - public void Process(DocumentProcessorContext context) - { - context.Document.BasePath = Constants.ApiPrefix; - - context.Document.Info.Version = version; - context.Document.Info.ExtensionData = new Dictionary - { - ["x-logo"] = new { url = logoUrl, backgroundColor } - }; - - context.Document.ExternalDocumentation = documentation; - } - } -} diff --git a/src/Squidex/Areas/Api/Config/OpenApi/OpenApiExtensions.cs b/src/Squidex/Areas/Api/Config/OpenApi/OpenApiExtensions.cs deleted file mode 100644 index 9e6e9026f..000000000 --- a/src/Squidex/Areas/Api/Config/OpenApi/OpenApiExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Builder; - -namespace Squidex.Areas.Api.Config.OpenApi -{ - public static class OpenApiExtensions - { - public static void UseMyOpenApi(this IApplicationBuilder app) - { - app.UseOpenApi(); - } - } -} diff --git a/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs b/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs deleted file mode 100644 index 0c7e3de7d..000000000 --- a/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; -using NJsonSchema; -using NJsonSchema.Generation.TypeMappers; -using NodaTime; -using NSwag.Generation; -using NSwag.Generation.Processors; -using Squidex.Areas.Api.Controllers.Contents.Generator; -using Squidex.Areas.Api.Controllers.Rules.Models; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; - -namespace Squidex.Areas.Api.Config.OpenApi -{ - public static class OpenApiServices - { - public static void AddMyOpenApiSettings(this IServiceCollection services) - { - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddOpenApiDocument(settings => - { - settings.ConfigureName(); - settings.ConfigureSchemaSettings(); - - settings.OperationProcessors.Add(new ODataQueryParamsProcessor("/apps/{app}/assets", "assets", false)); - }); - - services.AddTransient(); - } - - public static void ConfigureName(this T settings) where T : OpenApiDocumentGeneratorSettings - { - settings.Title = "Squidex API"; - } - - public static void ConfigureSchemaSettings(this T settings) where T : OpenApiDocumentGeneratorSettings - { - settings.TypeMappers = new List - { - new PrimitiveTypeMapper(typeof(Instant), schema => - { - schema.Type = JsonObjectType.String; - schema.Format = JsonFormatStrings.DateTime; - }), - - new PrimitiveTypeMapper(typeof(Language), s => s.Type = JsonObjectType.String), - new PrimitiveTypeMapper(typeof(RefToken), s => s.Type = JsonObjectType.String), - new PrimitiveTypeMapper(typeof(Status), s => s.Type = JsonObjectType.String) - }; - } - } -} diff --git a/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs b/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs deleted file mode 100644 index a0fa2eb2f..000000000 --- a/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Microsoft.AspNetCore.Authorization; -using NSwag; -using NSwag.Generation.Processors; -using NSwag.Generation.Processors.Contexts; -using Squidex.Web; - -namespace Squidex.Areas.Api.Config.OpenApi -{ - public sealed class ScopesProcessor : IOperationProcessor - { - public bool Process(OperationProcessorContext context) - { - if (context.OperationDescription.Operation.Security == null) - { - context.OperationDescription.Operation.Security = new List(); - } - - var permissionAttribute = context.MethodInfo.GetCustomAttribute(); - - if (permissionAttribute != null) - { - context.OperationDescription.Operation.Security.Add(new OpenApiSecurityRequirement - { - [Constants.SecurityDefinition] = permissionAttribute.PermissionIds - }); - } - else - { - var authorizeAttributes = - context.MethodInfo.GetCustomAttributes(true).Union( - context.MethodInfo.DeclaringType.GetCustomAttributes(true)) - .ToArray(); - - if (authorizeAttributes.Any()) - { - var scopes = authorizeAttributes.Where(a => a.Roles != null).SelectMany(a => a.Roles.Split(',')).Distinct().ToList(); - - context.OperationDescription.Operation.Security.Add(new OpenApiSecurityRequirement - { - [Constants.SecurityDefinition] = scopes - }); - } - } - - return true; - } - } -} diff --git a/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs b/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs deleted file mode 100644 index 3a9ef5d33..000000000 --- a/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Text.RegularExpressions; -using Namotion.Reflection; -using NSwag; -using NSwag.Generation.Processors; -using NSwag.Generation.Processors.Contexts; - -namespace Squidex.Areas.Api.Config.OpenApi -{ - public sealed class XmlResponseTypesProcessor : IOperationProcessor - { - private static readonly Regex ResponseRegex = new Regex("(?[0-9]{3}) => (?.*)", RegexOptions.Compiled); - - public bool Process(OperationProcessorContext context) - { - var operation = context.OperationDescription.Operation; - - var returnsDescription = context.MethodInfo.GetXmlDocsTag("returns"); - - if (!string.IsNullOrWhiteSpace(returnsDescription)) - { - foreach (Match match in ResponseRegex.Matches(returnsDescription)) - { - var statusCode = match.Groups["Code"].Value; - - if (!operation.Responses.TryGetValue(statusCode, out var response)) - { - response = new OpenApiResponse(); - - operation.Responses[statusCode] = response; - } - - var description = match.Groups["Description"].Value; - - if (description.Contains("=>")) - { - throw new InvalidOperationException("Description not formatted correcly."); - } - - response.Description = description; - } - } - - return true; - } - } -} \ No newline at end of file diff --git a/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs b/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs deleted file mode 100644 index cb4a04043..000000000 --- a/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; -using System.Reflection; -using Microsoft.AspNetCore.Mvc; -using Namotion.Reflection; -using NSwag.Generation.Processors; -using NSwag.Generation.Processors.Contexts; - -namespace Squidex.Areas.Api.Config.OpenApi -{ - public sealed class XmlTagProcessor : IDocumentProcessor - { - public void Process(DocumentProcessorContext context) - { - foreach (var controllerType in context.ControllerTypes) - { - var attribute = controllerType.GetCustomAttribute(); - - if (attribute != null) - { - var tag = context.Document.Tags.FirstOrDefault(x => x.Name == attribute.GroupName); - - if (tag != null) - { - var description = controllerType.GetXmlDocsSummary(); - - if (description != null) - { - tag.Description = tag.Description ?? string.Empty; - - if (!tag.Description.Contains(description)) - { - tag.Description += "\n\n" + description; - } - } - } - } - } - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs deleted file mode 100644 index 3a5a92dbf..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ /dev/null @@ -1,302 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; -using NSwag.Annotations; -using Squidex.Areas.Api.Controllers.Apps.Models; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.Validation; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Apps -{ - /// - /// Manages and configures apps. - /// - [ApiExplorerSettings(GroupName = nameof(Apps))] - public sealed class AppsController : ApiController - { - private readonly IAssetStore assetStore; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - private readonly IAppProvider appProvider; - private readonly IAppPlansProvider appPlansProvider; - - public AppsController(ICommandBus commandBus, - IAssetStore assetStore, - IAssetThumbnailGenerator assetThumbnailGenerator, - IAppProvider appProvider, - IAppPlansProvider appPlansProvider) - : base(commandBus) - { - this.assetStore = assetStore; - this.assetThumbnailGenerator = assetThumbnailGenerator; - this.appProvider = appProvider; - this.appPlansProvider = appPlansProvider; - } - - /// - /// Get your apps. - /// - /// - /// 200 => Apps returned. - /// - /// - /// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow). - /// You will retrieve all apps, where you are assigned as a contributor. - /// - [HttpGet] - [Route("apps/")] - [ProducesResponseType(typeof(AppDto[]), 200)] - [ApiPermission] - [ApiCosts(0)] - public async Task GetApps() - { - var userOrClientId = HttpContext.User.UserOrClientId(); - var userPermissions = HttpContext.Permissions(); - - var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions); - - var response = Deferred.Response(() => - { - return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider, this)).ToArray(); - }); - - Response.Headers[HeaderNames.ETag] = apps.ToEtag(); - - return Ok(response); - } - - /// - /// Create a new app. - /// - /// The app object that needs to be added to squidex. - /// - /// 201 => App created. - /// 400 => App request not valid. - /// 409 => App name is already in use. - /// - /// - /// You can only create an app when you are authenticated as a user (OpenID implicit flow). - /// You will be assigned as owner of the new app automatically. - /// - [HttpPost] - [Route("apps/")] - [ProducesResponseType(typeof(AppDto), 201)] - [ApiPermission] - [ApiCosts(0)] - public async Task PostApp([FromBody] CreateAppDto request) - { - var response = await InvokeCommandAsync(request.ToCommand()); - - return CreatedAtAction(nameof(GetApps), response); - } - - /// - /// Update the app. - /// - /// The name of the app to update. - /// The values to update. - /// - /// 200 => App updated. - /// 404 => App not found. - /// - [HttpPut] - [Route("apps/{app}/")] - [ProducesResponseType(typeof(AppDto), 200)] - [ApiPermission(Permissions.AppUpdateGeneral)] - [ApiCosts(0)] - public async Task UpdateApp(string app, [FromBody] UpdateAppDto request) - { - var response = await InvokeCommandAsync(request.ToCommand()); - - return Ok(response); - } - - /// - /// Get the app image. - /// - /// The name of the app to update. - /// The file to upload. - /// - /// 200 => App image uploaded. - /// 404 => App not found. - /// - [HttpPost] - [Route("apps/{app}/image")] - [ProducesResponseType(typeof(AppDto), 201)] - [ApiPermission(Permissions.AppUpdateImage)] - [ApiCosts(0)] - public async Task UploadImage(string app, [OpenApiIgnore] List file) - { - var response = await InvokeCommandAsync(CreateCommand(file)); - - return Ok(response); - } - - /// - /// Get the app image. - /// - /// The name of the app. - /// - /// 200 => App image found and content or (resized) image returned. - /// 404 => App not found. - /// - [HttpGet] - [Route("apps/{app}/image")] - [ProducesResponseType(typeof(FileResult), 200)] - [AllowAnonymous] - [ApiCosts(0)] - public IActionResult GetImage(string app) - { - if (App.Image == null) - { - return NotFound(); - } - - var etag = App.Image.Etag; - - Response.Headers[HeaderNames.ETag] = etag; - - var handler = new Func(async bodyStream => - { - var assetId = App.Id.ToString(); - var assetResizedId = $"{assetId}_{etag}_Resized"; - - try - { - await assetStore.DownloadAsync(assetResizedId, bodyStream); - } - catch (AssetNotFoundException) - { - using (Profiler.Trace("Resize")) - { - using (var sourceStream = GetTempStream()) - { - using (var destinationStream = GetTempStream()) - { - using (Profiler.Trace("ResizeDownload")) - { - await assetStore.DownloadAsync(assetId, sourceStream); - sourceStream.Position = 0; - } - - using (Profiler.Trace("ResizeImage")) - { - await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, 150, 150, "Crop"); - destinationStream.Position = 0; - } - - using (Profiler.Trace("ResizeUpload")) - { - await assetStore.UploadAsync(assetResizedId, destinationStream); - destinationStream.Position = 0; - } - - await destinationStream.CopyToAsync(bodyStream); - } - } - } - } - }); - - return new FileCallbackResult(App.Image.MimeType, null, true, handler); - } - - /// - /// Remove the app image. - /// - /// The name of the app to update. - /// - /// 200 => App image removed. - /// 404 => App not found. - /// - [HttpDelete] - [Route("apps/{app}/image")] - [ProducesResponseType(typeof(AppDto), 201)] - [ApiPermission(Permissions.AppUpdate)] - [ApiCosts(0)] - public async Task DeleteImage(string app) - { - var response = await InvokeCommandAsync(new RemoveAppImage()); - - return Ok(response); - } - - /// - /// Archive the app. - /// - /// The name of the app to archive. - /// - /// 204 => App archived. - /// 404 => App not found. - /// - [HttpDelete] - [Route("apps/{app}/")] - [ApiPermission(Permissions.AppDelete)] - [ApiCosts(0)] - public async Task DeleteApp(string app) - { - await CommandBus.PublishAsync(new ArchiveApp()); - - return NoContent(); - } - - private async Task InvokeCommandAsync(ICommand command) - { - var context = await CommandBus.PublishAsync(command); - - var userOrClientId = HttpContext.User.UserOrClientId(); - var userPermissions = HttpContext.Permissions(); - - var result = context.Result(); - var response = AppDto.FromApp(result, userOrClientId, userPermissions, appPlansProvider, this); - - return response; - } - - private static UploadAppImage CreateCommand(IReadOnlyList file) - { - if (file.Count != 1) - { - var error = new ValidationError($"Can only upload one file, found {file.Count} files."); - - throw new ValidationException("Cannot create asset.", error); - } - - return new UploadAppImage { File = file[0].ToAssetFile() }; - } - - private static FileStream GetTempStream() - { - var tempFileName = Path.GetTempFileName(); - - return new FileStream(tempFileName, - FileMode.Create, - FileAccess.ReadWrite, - FileShare.Delete, 1024 * 16, - FileOptions.Asynchronous | - FileOptions.DeleteOnClose | - FileOptions.SequentialScan); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs deleted file mode 100644 index 526b9c631..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ /dev/null @@ -1,244 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using NodaTime; -using Squidex.Areas.Api.Controllers.Assets; -using Squidex.Areas.Api.Controllers.Backups; -using Squidex.Areas.Api.Controllers.Ping; -using Squidex.Areas.Api.Controllers.Plans; -using Squidex.Areas.Api.Controllers.Rules; -using Squidex.Areas.Api.Controllers.Schemas; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Security; -using Squidex.Shared; -using Squidex.Web; -using AllPermissions = Squidex.Shared.Permissions; - -#pragma warning disable RECS0033 // Convert 'if' to '||' expression - -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class AppDto : Resource - { - /// - /// The name of the app. - /// - [Required] - [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] - public string Name { get; set; } - - /// - /// The optional label of the app. - /// - public string Label { get; set; } - - /// - /// The optional description of the app. - /// - public string Description { get; set; } - - /// - /// The version of the app. - /// - public long Version { get; set; } - - /// - /// The id of the app. - /// - public Guid Id { get; set; } - - /// - /// The timestamp when the app has been created. - /// - public Instant Created { get; set; } - - /// - /// The timestamp when the app has been modified last. - /// - public Instant LastModified { get; set; } - - /// - /// The permission level of the user. - /// - public IEnumerable Permissions { get; set; } - - /// - /// Indicates if the user can access the api. - /// - public bool CanAccessApi { get; set; } - - /// - /// Indicates if the user can access at least one content. - /// - public bool CanAccessContent { get; set; } - - /// - /// Gets the current plan name. - /// - public string PlanName { get; set; } - - /// - /// Gets the next plan name. - /// - public string PlanUpgrade { get; set; } - - public static AppDto FromApp(IAppEntity app, string userId, PermissionSet userPermissions, IAppPlansProvider plans, ApiController controller) - { - var permissions = GetPermissions(app, userId, userPermissions); - - var result = SimpleMapper.Map(app, new AppDto()); - - result.Permissions = permissions.ToIds(); - - if (controller.Includes(AllPermissions.ForApp(AllPermissions.AppApi, app.Name), permissions)) - { - result.CanAccessApi = true; - } - - if (controller.Includes(AllPermissions.ForApp(AllPermissions.AppContents, app.Name), permissions)) - { - result.CanAccessContent = true; - } - - result.SetPlan(app, plans, controller, permissions); - result.SetImage(app, controller); - - return result.CreateLinks(controller, permissions); - } - - private static PermissionSet GetPermissions(IAppEntity app, string userId, PermissionSet userPermissions) - { - var permissions = new List(); - - if (app.Contributors.TryGetValue(userId, out var roleName) && app.Roles.TryGet(app.Name, roleName, out var role)) - { - permissions.AddRange(role.Permissions); - } - - if (userPermissions != null) - { - permissions.AddRange(userPermissions.ToAppPermissions(app.Name)); - } - - return new PermissionSet(permissions); - } - - private void SetPlan(IAppEntity app, IAppPlansProvider plans, ApiController controller, PermissionSet permissions) - { - if (controller.HasPermission(AllPermissions.AppPlansChange, app.Name, additional: permissions)) - { - PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name; - } - - PlanName = plans.GetPlanForApp(app)?.Name; - } - - private void SetImage(IAppEntity app, ApiController controller) - { - if (app.Image != null) - { - AddGetLink("image", controller.Url(x => nameof(x.GetImage), new { app = app.Name })); - } - } - - private AppDto CreateLinks(ApiController controller, PermissionSet permissions) - { - var values = new { app = Name }; - - AddGetLink("ping", controller.Url(x => nameof(x.GetAppPing), values)); - - if (controller.HasPermission(AllPermissions.AppDelete, Name, additional: permissions)) - { - AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteApp), values)); - } - - if (controller.HasPermission(AllPermissions.AppUpdateGeneral, Name, additional: permissions)) - { - AddPutLink("update", controller.Url(x => nameof(x.UpdateApp), values)); - } - - if (controller.HasPermission(AllPermissions.AppUpdateImage, Name, additional: permissions)) - { - AddPostLink("image/upload", controller.Url(x => nameof(x.UploadImage), values)); - - AddDeleteLink("image/delete", controller.Url(x => nameof(x.DeleteImage), values)); - } - - if (controller.HasPermission(AllPermissions.AppAssetsRead, Name, additional: permissions)) - { - AddGetLink("assets", controller.Url(x => nameof(x.GetAssets), values)); - } - - if (controller.HasPermission(AllPermissions.AppBackupsRead, Name, additional: permissions)) - { - AddGetLink("backups", controller.Url(x => nameof(x.GetBackups), values)); - } - - if (controller.HasPermission(AllPermissions.AppClientsRead, Name, additional: permissions)) - { - AddGetLink("clients", controller.Url(x => nameof(x.GetClients), values)); - } - - if (controller.HasPermission(AllPermissions.AppContributorsRead, Name, additional: permissions)) - { - AddGetLink("contributors", controller.Url(x => nameof(x.GetContributors), values)); - } - - if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) - { - AddGetLink("languages", controller.Url(x => nameof(x.GetLanguages), values)); - } - - if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) - { - AddGetLink("patterns", controller.Url(x => nameof(x.GetPatterns), values)); - } - - if (controller.HasPermission(AllPermissions.AppPlansRead, Name, additional: permissions)) - { - AddGetLink("plans", controller.Url(x => nameof(x.GetPlans), values)); - } - - if (controller.HasPermission(AllPermissions.AppRolesRead, Name, additional: permissions)) - { - AddGetLink("roles", controller.Url(x => nameof(x.GetRoles), values)); - } - - if (controller.HasPermission(AllPermissions.AppRulesRead, Name, additional: permissions)) - { - AddGetLink("rules", controller.Url(x => nameof(x.GetRules), values)); - } - - if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) - { - AddGetLink("schemas", controller.Url(x => nameof(x.GetSchemas), values)); - } - - if (controller.HasPermission(AllPermissions.AppWorkflowsRead, Name, additional: permissions)) - { - AddGetLink("workflows", controller.Url(x => nameof(x.GetWorkflows), values)); - } - - if (controller.HasPermission(AllPermissions.AppSchemasCreate, Name, additional: permissions)) - { - AddPostLink("schemas/create", controller.Url(x => nameof(x.PostSchema), values)); - } - - if (controller.HasPermission(AllPermissions.AppAssetsCreate, Name, additional: permissions)) - { - AddPostLink("assets/create", controller.Url(x => nameof(x.PostSchema), values)); - } - - return this; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs deleted file mode 100644 index 9efa1133a..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using Squidex.Shared; -using Squidex.Shared.Users; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class ContributorDto : Resource - { - private const string NotFound = "- not found -"; - - /// - /// The id of the user that contributes to the app. - /// - [Required] - public string ContributorId { get; set; } - - /// - /// The display name. - /// - [Required] - public string ContributorName { get; set; } - - /// - /// The role of the contributor. - /// - public string Role { get; set; } - - public static ContributorDto FromIdAndRole(string id, string role) - { - var result = new ContributorDto { ContributorId = id, Role = role }; - - return result; - } - - public ContributorDto WithUser(IDictionary users) - { - if (users.TryGetValue(ContributorId, out var user)) - { - ContributorName = user.DisplayName(); - } - else - { - ContributorName = NotFound; - } - - return this; - } - - public ContributorDto WithLinks(ApiController controller, string app) - { - if (!controller.IsUser(ContributorId)) - { - if (controller.HasPermission(Permissions.AppContributorsAssign, app)) - { - AddPostLink("update", controller.Url(x => nameof(x.PostContributor), new { app })); - } - - if (controller.HasPermission(Permissions.AppContributorsRevoke, app)) - { - AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteContributor), new { app, id = ContributorId })); - } - } - - return this; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs deleted file mode 100644 index e22d45589..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps.Commands; - -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class UpdateWorkflowDto - { - /// - /// The name of the workflow. - /// - public string Name { get; set; } - - /// - /// The workflow steps. - /// - [Required] - public Dictionary Steps { get; set; } - - /// - /// The schema ids. - /// - public List SchemaIds { get; set; } - - /// - /// The initial step. - /// - public Status Initial { get; set; } - - public UpdateWorkflow ToCommand(Guid id) - { - var workflow = new Workflow( - Initial, - Steps?.ToDictionary( - x => x.Key, - x => x.Value?.ToStep()), - SchemaIds, - Name); - - return new UpdateWorkflow { WorkflowId = id, Workflow = workflow }; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs deleted file mode 100644 index 85a41e991..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure.Reflection; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class WorkflowDto : Resource - { - /// - /// The workflow id. - /// - public Guid Id { get; set; } - - /// - /// The name of the workflow. - /// - public string Name { get; set; } - - /// - /// The workflow steps. - /// - [Required] - public Dictionary Steps { get; set; } - - /// - /// The schema ids. - /// - public IReadOnlyList SchemaIds { get; set; } - - /// - /// The initial step. - /// - public Status Initial { get; set; } - - public static WorkflowDto FromWorkflow(Guid id, Workflow workflow) - { - var result = SimpleMapper.Map(workflow, new WorkflowDto - { - Steps = workflow.Steps.ToDictionary( - x => x.Key, - x => WorkflowStepDto.FromWorkflowStep(x.Value)), - Id = id - }); - - return result; - } - - public WorkflowDto WithLinks(ApiController controller, string app) - { - var values = new { app, id = Id }; - - if (controller.HasPermission(Permissions.AppWorkflowsUpdate, app)) - { - AddPutLink("update", controller.Url(x => nameof(x.PutWorkflow), values)); - } - - if (controller.HasPermission(Permissions.AppWorkflowsDelete, app)) - { - AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteWorkflow), values)); - } - - return this; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs deleted file mode 100644 index 3007c454b..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class WorkflowStepDto - { - /// - /// The transitions. - /// - [Required] - public Dictionary Transitions { get; set; } - - /// - /// The optional color. - /// - public string Color { get; set; } - - /// - /// Indicates if updates should not be allowed. - /// - public bool NoUpdate { get; set; } - - public static WorkflowStepDto FromWorkflowStep(WorkflowStep step) - { - if (step == null) - { - return null; - } - - return SimpleMapper.Map(step, new WorkflowStepDto - { - Transitions = step.Transitions.ToDictionary( - y => y.Key, - y => WorkflowTransitionDto.FromWorkflowTransition(y.Value)) - }); - } - - public WorkflowStep ToStep() - { - return new WorkflowStep( - Transitions?.ToDictionary( - y => y.Key, - y => y.Value?.ToTransition()), - Color, NoUpdate); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs deleted file mode 100644 index 9b7d221f9..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.ObjectModel; -using Squidex.Domain.Apps.Core.Contents; - -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class WorkflowTransitionDto - { - /// - /// The optional expression. - /// - public string Expression { get; set; } - - /// - /// The optional restricted role. - /// - public ReadOnlyCollection Roles { get; set; } - - public static WorkflowTransitionDto FromWorkflowTransition(WorkflowTransition transition) - { - if (transition == null) - { - return null; - } - - return new WorkflowTransitionDto { Expression = transition.Expression, Roles = transition.Roles }; - } - - public WorkflowTransition ToTransition() - { - return new WorkflowTransition(Expression, Roles); - } - } -} \ No newline at end of file diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs deleted file mode 100644 index dcfcd5491..000000000 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ /dev/null @@ -1,202 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; -using Squidex.Areas.Api.Controllers.Assets.Models; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Web; - -#pragma warning disable 1573 - -namespace Squidex.Areas.Api.Controllers.Assets -{ - /// - /// Uploads and retrieves assets. - /// - [ApiExplorerSettings(GroupName = nameof(Assets))] - public sealed class AssetContentController : ApiController - { - private readonly IAssetStore assetStore; - private readonly IAssetRepository assetRepository; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - - public AssetContentController( - ICommandBus commandBus, - IAssetStore assetStore, - IAssetRepository assetRepository, - IAssetThumbnailGenerator assetThumbnailGenerator) - : base(commandBus) - { - this.assetStore = assetStore; - this.assetRepository = assetRepository; - this.assetThumbnailGenerator = assetThumbnailGenerator; - } - - /// - /// Get the asset content. - /// - /// The name of the app. - /// The id or slug of the asset. - /// Optional suffix that can be used to seo-optimize the link to the image Has not effect. - /// The query string parameters. - /// - /// 200 => Asset found and content or (resized) image returned. - /// 404 => Asset or app not found. - /// - [HttpGet] - [Route("assets/{app}/{idOrSlug}/{*more}")] - [ProducesResponseType(typeof(FileResult), 200)] - [ApiCosts(0.5)] - [AllowAnonymous] - public async Task GetAssetContentBySlug(string app, string idOrSlug, string more, [FromQuery] AssetQuery query) - { - IAssetEntity asset; - - if (Guid.TryParse(idOrSlug, out var guid)) - { - asset = await assetRepository.FindAssetAsync(guid); - } - else - { - asset = await assetRepository.FindAssetBySlugAsync(App.Id, idOrSlug); - } - - return DeliverAsset(asset, query); - } - - /// - /// Get the asset content. - /// - /// The id of the asset. - /// The query string parameters. - /// - /// 200 => Asset found and content or (resized) image returned. - /// 404 => Asset or app not found. - /// - [HttpGet] - [Route("assets/{id}/")] - [ProducesResponseType(typeof(FileResult), 200)] - [ApiCosts(0.5)] - public async Task GetAssetContent(Guid id, [FromQuery] AssetQuery query) - { - var asset = await assetRepository.FindAssetAsync(id); - - return DeliverAsset(asset, query); - } - - private IActionResult DeliverAsset(IAssetEntity asset, AssetQuery query) - { - query = query ?? new AssetQuery(); - - if (asset == null || asset.FileVersion < query.Version) - { - return NotFound(); - } - - var fileVersion = query.Version; - - if (fileVersion <= EtagVersion.Any) - { - fileVersion = asset.FileVersion; - } - - Response.Headers[HeaderNames.ETag] = fileVersion.ToString(); - - if (query.CacheDuration > 0) - { - Response.Headers[HeaderNames.CacheControl] = $"public,max-age={query.CacheDuration}"; - } - - var handler = new Func(async bodyStream => - { - var assetId = asset.Id.ToString(); - - if (asset.IsImage && query.ShouldResize()) - { - var assetSuffix = $"{query.Width}_{query.Height}_{query.Mode}"; - - if (query.Quality.HasValue) - { - assetSuffix += $"_{query.Quality}"; - } - - try - { - await assetStore.DownloadAsync(assetId, fileVersion, assetSuffix, bodyStream); - } - catch (AssetNotFoundException) - { - using (Profiler.Trace("Resize")) - { - using (var sourceStream = GetTempStream()) - { - using (var destinationStream = GetTempStream()) - { - using (Profiler.Trace("ResizeDownload")) - { - await assetStore.DownloadAsync(assetId, fileVersion, null, sourceStream); - sourceStream.Position = 0; - } - - using (Profiler.Trace("ResizeImage")) - { - await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, query.Width, query.Height, query.Mode, query.Quality); - destinationStream.Position = 0; - } - - using (Profiler.Trace("ResizeUpload")) - { - await assetStore.UploadAsync(assetId, fileVersion, assetSuffix, destinationStream); - destinationStream.Position = 0; - } - - await destinationStream.CopyToAsync(bodyStream); - } - } - } - } - } - else - { - await assetStore.DownloadAsync(assetId, fileVersion, null, bodyStream); - } - }); - - if (query.Download == 1) - { - return new FileCallbackResult(asset.MimeType, asset.FileName, true, handler); - } - else - { - return new FileCallbackResult(asset.MimeType, null, true, handler); - } - } - - private static FileStream GetTempStream() - { - var tempFileName = Path.GetTempFileName(); - - return new FileStream(tempFileName, - FileMode.Create, - FileAccess.ReadWrite, - FileShare.Delete, 1024 * 16, - FileOptions.Asynchronous | - FileOptions.DeleteOnClose | - FileOptions.SequentialScan); - } - } -} \ No newline at end of file diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs deleted file mode 100644 index 394c91a5b..000000000 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ /dev/null @@ -1,319 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; -using NSwag.Annotations; -using Squidex.Areas.Api.Controllers.Assets.Models; -using Squidex.Areas.Api.Controllers.Contents; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Validation; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Assets -{ - /// - /// Uploads and retrieves assets. - /// - [ApiExplorerSettings(GroupName = nameof(Assets))] - public sealed class AssetsController : ApiController - { - private readonly IAssetQueryService assetQuery; - private readonly IAssetUsageTracker assetStatsRepository; - private readonly IAppPlansProvider appPlansProvider; - private readonly MyContentsControllerOptions controllerOptions; - private readonly ITagService tagService; - private readonly AssetOptions assetOptions; - - public AssetsController( - ICommandBus commandBus, - IAssetQueryService assetQuery, - IAssetUsageTracker assetStatsRepository, - IAppPlansProvider appPlansProvider, - IOptions assetOptions, - IOptions controllerOptions, - ITagService tagService) - : base(commandBus) - { - this.assetOptions = assetOptions.Value; - this.assetQuery = assetQuery; - this.assetStatsRepository = assetStatsRepository; - this.appPlansProvider = appPlansProvider; - this.controllerOptions = controllerOptions.Value; - this.tagService = tagService; - } - - /// - /// Get assets tags. - /// - /// The name of the app. - /// - /// 200 => Assets returned. - /// 404 => App not found. - /// - /// - /// Get all tags for assets. - /// - [HttpGet] - [Route("apps/{app}/assets/tags")] - [ProducesResponseType(typeof(Dictionary), 200)] - [ApiPermission(Permissions.AppAssetsRead)] - [ApiCosts(1)] - public async Task GetTags(string app) - { - var tags = await tagService.GetTagsAsync(AppId, TagGroups.Assets); - - Response.Headers[HeaderNames.ETag] = tags.Version.ToString(); - - return Ok(tags); - } - - /// - /// Get assets. - /// - /// The name of the app. - /// The optional asset ids. - /// The optional json query. - /// - /// 200 => Assets returned. - /// 404 => App not found. - /// - /// - /// Get all assets for the app. - /// - [HttpGet] - [Route("apps/{app}/assets/")] - [ProducesResponseType(typeof(AssetsDto), 200)] - [ApiPermission(Permissions.AppAssetsRead)] - [ApiCosts(1)] - public async Task GetAssets(string app, [FromQuery] string ids = null, [FromQuery] string q = null) - { - var assets = await assetQuery.QueryAsync(Context, - Q.Empty - .WithIds(ids) - .WithJsonQuery(q) - .WithODataQuery(Request.QueryString.ToString())); - - var response = Deferred.Response(() => - { - return AssetsDto.FromAssets(assets, this, app); - }); - - if (controllerOptions.EnableSurrogateKeys && assets.Count <= controllerOptions.MaxItemsForSurrogateKeys) - { - Response.Headers["Surrogate-Key"] = assets.ToSurrogateKeys(); - } - - Response.Headers[HeaderNames.ETag] = assets.ToEtag(); - - return Ok(response); - } - - /// - /// Get an asset by id. - /// - /// The name of the app. - /// The id of the asset to retrieve. - /// - /// 200 => Asset found. - /// 404 => Asset or app not found. - /// - [HttpGet] - [Route("apps/{app}/assets/{id}/")] - [ProducesResponseType(typeof(AssetsDto), 200)] - [ApiPermission(Permissions.AppAssetsRead)] - [ApiCosts(1)] - public async Task GetAsset(string app, Guid id) - { - var asset = await assetQuery.FindAssetAsync(Context, id); - - if (asset == null) - { - return NotFound(); - } - - var response = Deferred.Response(() => - { - return AssetDto.FromAsset(asset, this, app); - }); - - if (controllerOptions.EnableSurrogateKeys) - { - Response.Headers["Surrogate-Key"] = asset.ToSurrogateKey(); - } - - Response.Headers[HeaderNames.ETag] = asset.ToEtag(); - - return Ok(response); - } - - /// - /// Upload a new asset. - /// - /// The name of the app. - /// The file to upload. - /// - /// 201 => Asset created. - /// 404 => App not found. - /// 400 => Asset exceeds the maximum size. - /// - /// - /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. - /// - [HttpPost] - [Route("apps/{app}/assets/")] - [ProducesResponseType(typeof(AssetDto), 201)] - [AssetRequestSizeLimit] - [ApiPermission(Permissions.AppAssetsCreate)] - [ApiCosts(1)] - public async Task PostAsset(string app, [OpenApiIgnore] List file) - { - var assetFile = await CheckAssetFileAsync(file); - - var command = new CreateAsset { File = assetFile }; - - var response = await InvokeCommandAsync(app, command); - - return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); - } - - /// - /// Replace asset content. - /// - /// The name of the app. - /// The id of the asset. - /// The file to upload. - /// - /// 200 => Asset updated. - /// 404 => Asset or app not found. - /// 400 => Asset exceeds the maximum size. - /// - /// - /// Use multipart request to upload an asset. - /// - [HttpPut] - [Route("apps/{app}/assets/{id}/content/")] - [ProducesResponseType(typeof(AssetDto), 200)] - [ApiPermission(Permissions.AppAssetsUpdate)] - [ApiCosts(1)] - public async Task PutAssetContent(string app, Guid id, [OpenApiIgnore] List file) - { - var assetFile = await CheckAssetFileAsync(file); - - var command = new UpdateAsset { File = assetFile, AssetId = id }; - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Updates the asset. - /// - /// The name of the app. - /// The id of the asset. - /// The asset object that needs to updated. - /// - /// 200 => Asset updated. - /// 400 => Asset name not valid. - /// 404 => Asset or app not found. - /// - [HttpPut] - [Route("apps/{app}/assets/{id}/")] - [ProducesResponseType(typeof(AssetDto), 200)] - [AssetRequestSizeLimit] - [ApiPermission(Permissions.AppAssetsUpdate)] - [ApiCosts(1)] - public async Task PutAsset(string app, Guid id, [FromBody] AnnotateAssetDto request) - { - var command = request.ToCommand(id); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Delete an asset. - /// - /// The name of the app. - /// The id of the asset to delete. - /// - /// 204 => Asset deleted. - /// 404 => Asset or app not found. - /// - [HttpDelete] - [Route("apps/{app}/assets/{id}/")] - [ApiPermission(Permissions.AppAssetsDelete)] - [ApiCosts(1)] - public async Task DeleteAsset(string app, Guid id) - { - await CommandBus.PublishAsync(new DeleteAsset { AssetId = id }); - - return NoContent(); - } - - private async Task InvokeCommandAsync(string app, ICommand command) - { - var context = await CommandBus.PublishAsync(command); - - if (context.PlainResult is AssetCreatedResult created) - { - return AssetDto.FromAsset(created.Asset, this, app, created.IsDuplicate); - } - else - { - return AssetDto.FromAsset(context.Result(), this, app); - } - } - - private async Task CheckAssetFileAsync(IReadOnlyList file) - { - if (file.Count != 1) - { - var error = new ValidationError($"Can only upload one file, found {file.Count} files."); - - throw new ValidationException("Cannot create asset.", error); - } - - var formFile = file[0]; - - if (formFile.Length > assetOptions.MaxSize) - { - var error = new ValidationError($"File cannot be bigger than {assetOptions.MaxSize.ToReadableSize()}."); - - throw new ValidationException("Cannot create asset.", error); - } - - var plan = appPlansProvider.GetPlanForApp(App); - - var currentSize = await assetStatsRepository.GetTotalSizeAsync(AppId); - - if (plan.MaxAssetSize > 0 && plan.MaxAssetSize < currentSize + formFile.Length) - { - var error = new ValidationError("You have reached your max asset size."); - - throw new ValidationException("Cannot create asset.", error); - } - - return formFile.ToAssetFile(); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs b/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs deleted file mode 100644 index f648c7050..000000000 --- a/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Orleans; -using Squidex.Areas.Api.Controllers.Backups.Models; -using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Security; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Backups -{ - /// - /// Manages backups for apps. - /// - [ApiExplorerSettings(GroupName = nameof(Backups))] - public class RestoreController : ApiController - { - private readonly IGrainFactory grainFactory; - - public RestoreController(ICommandBus commandBus, IGrainFactory grainFactory) - : base(commandBus) - { - this.grainFactory = grainFactory; - } - - /// - /// Get current restore status. - /// - /// - /// 200 => Status returned. - /// - [HttpGet] - [Route("apps/restore/")] - [ProducesResponseType(typeof(RestoreJobDto), 200)] - [ApiPermission(Permissions.AdminRestore)] - public async Task GetRestoreJob() - { - var restoreGrain = grainFactory.GetGrain(SingleGrain.Id); - - var job = await restoreGrain.GetJobAsync(); - - if (job.Value == null) - { - return NotFound(); - } - - var response = RestoreJobDto.FromJob(job.Value); - - return Ok(response); - } - - /// - /// Restore a backup. - /// - /// The backup to restore. - /// - /// 204 => Restore operation started. - /// - [HttpPost] - [Route("apps/restore/")] - [ApiPermission(Permissions.AdminRestore)] - public async Task PostRestoreJob([FromBody] RestoreRequestDto request) - { - var restoreGrain = grainFactory.GetGrain(SingleGrain.Id); - - await restoreGrain.RestoreAsync(request.Url, User.Token(), request.Name); - - return NoContent(); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs deleted file mode 100644 index 2f9b55f1d..000000000 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ /dev/null @@ -1,457 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; -using Squidex.Areas.Api.Controllers.Contents.Models; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.GraphQL; -using Squidex.Infrastructure.Commands; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Contents -{ - public sealed class ContentsController : ApiController - { - private readonly MyContentsControllerOptions controllerOptions; - private readonly IContentQueryService contentQuery; - private readonly IContentWorkflow contentWorkflow; - private readonly IGraphQLService graphQl; - - public ContentsController(ICommandBus commandBus, - IContentQueryService contentQuery, - IContentWorkflow contentWorkflow, - IGraphQLService graphQl, - IOptions controllerOptions) - : base(commandBus) - { - this.contentQuery = contentQuery; - this.contentWorkflow = contentWorkflow; - this.controllerOptions = controllerOptions.Value; - - this.graphQl = graphQl; - } - - /// - /// GraphQL endpoint. - /// - /// The name of the app. - /// The graphql query. - /// - /// 200 => Contents retrieved or mutated. - /// 404 => Schema or app not found. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpGet] - [HttpPost] - [Route("content/{app}/graphql/")] - [ApiPermission] - [ApiCosts(2)] - public async Task PostGraphQL(string app, [FromBody] GraphQLQuery query) - { - var (hasError, response) = await graphQl.QueryAsync(Context, query); - - if (hasError) - { - return BadRequest(response); - } - else - { - return Ok(response); - } - } - - /// - /// GraphQL endpoint (Batch). - /// - /// The name of the app. - /// The graphql queries. - /// - /// 200 => Contents retrieved or mutated. - /// 404 => Schema or app not found. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpGet] - [HttpPost] - [Route("content/{app}/graphql/batch")] - [ApiPermission] - [ApiCosts(2)] - public async Task PostGraphQLBatch(string app, [FromBody] GraphQLQuery[] batch) - { - var (hasError, response) = await graphQl.QueryAsync(Context, batch); - - if (hasError) - { - return BadRequest(response); - } - else - { - return Ok(response); - } - } - - /// - /// Queries contents. - /// - /// The name of the app. - /// The optional ids of the content to fetch. - /// - /// 200 => Contents retrieved. - /// 404 => App not found. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpGet] - [Route("content/{app}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission] - [ApiCosts(1)] - public async Task GetAllContents(string app, [FromQuery] string ids) - { - var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(ids).Ids); - - var response = Deferred.AsyncResponse(() => - { - return ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow); - }); - - if (ShouldProvideSurrogateKeys(contents)) - { - Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys(); - } - - Response.Headers[HeaderNames.ETag] = contents.ToEtag(); - - return Ok(response); - } - - /// - /// Queries contents. - /// - /// The name of the app. - /// The name of the schema. - /// The optional ids of the content to fetch. - /// The optional json query. - /// - /// 200 => Contents retrieved. - /// 404 => Schema or app not found. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpGet] - [Route("content/{app}/{name}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission] - [ApiCosts(1)] - public async Task GetContents(string app, string name, [FromQuery] string ids = null, [FromQuery] string q = null) - { - var schema = await contentQuery.GetSchemaOrThrowAsync(Context, name); - - var contents = await contentQuery.QueryAsync(Context, name, - Q.Empty - .WithIds(ids) - .WithJsonQuery(q) - .WithODataQuery(Request.QueryString.ToString())); - - var response = Deferred.AsyncResponse(async () => - { - return await ContentsDto.FromContentsAsync(contents, Context, this, schema, contentWorkflow); - }); - - if (ShouldProvideSurrogateKeys(contents)) - { - Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys(); - } - - Response.Headers[HeaderNames.ETag] = contents.ToEtag(); - - return Ok(response); - } - - /// - /// Get a content item. - /// - /// The name of the app. - /// The name of the schema. - /// The id of the content to fetch. - /// - /// 200 => Content found. - /// 404 => Content, schema or app not found. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpGet] - [Route("content/{app}/{name}/{id}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission] - [ApiCosts(1)] - public async Task GetContent(string app, string name, Guid id) - { - var content = await contentQuery.FindContentAsync(Context, name, id); - - var response = ContentDto.FromContent(Context, content, this); - - if (controllerOptions.EnableSurrogateKeys) - { - Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); - } - - Response.Headers[HeaderNames.ETag] = content.ToEtag(); - - return Ok(response); - } - - /// - /// Get a content by version. - /// - /// The name of the app. - /// The name of the schema. - /// The id of the content to fetch. - /// The version fo the content to fetch. - /// - /// 200 => Content found. - /// 404 => Content, schema or app not found. - /// 400 => Content data is not valid. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpGet] - [Route("content/{app}/{name}/{id}/{version}/")] - [ApiPermission(Permissions.AppContentsRead)] - [ApiCosts(1)] - public async Task GetContentVersion(string app, string name, Guid id, int version) - { - var content = await contentQuery.FindContentAsync(Context, name, id, version); - - var response = ContentDto.FromContent(Context, content, this); - - if (controllerOptions.EnableSurrogateKeys) - { - Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); - } - - Response.Headers[HeaderNames.ETag] = content.ToEtag(); - - return Ok(response.Data); - } - - /// - /// Create a content item. - /// - /// The name of the app. - /// The name of the schema. - /// The full data for the content item. - /// Indicates whether the content should be published immediately. - /// - /// 201 => Content created. - /// 404 => Content, schema or app not found. - /// 400 => Content data is not valid. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpPost] - [Route("content/{app}/{name}/")] - [ProducesResponseType(typeof(ContentsDto), 201)] - [ApiPermission(Permissions.AppContentsCreate)] - [ApiCosts(1)] - public async Task PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false) - { - await contentQuery.GetSchemaOrThrowAsync(Context, name); - - var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish }; - - var response = await InvokeCommandAsync(command); - - return CreatedAtAction(nameof(GetContent), new { app, id = command.ContentId }, response); - } - - /// - /// Update a content item. - /// - /// The name of the app. - /// The name of the schema. - /// The id of the content item to update. - /// The full data for the content item. - /// Indicates whether the update is a proposal. - /// - /// 200 => Content updated. - /// 404 => Content, schema or app not found. - /// 400 => Content data is not valid. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpPut] - [Route("content/{app}/{name}/{id}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission(Permissions.AppContentsUpdate)] - [ApiCosts(1)] - public async Task PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) - { - await contentQuery.GetSchemaOrThrowAsync(Context, name); - - var command = new UpdateContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// - /// Patchs a content item. - /// - /// The name of the app. - /// The name of the schema. - /// The id of the content item to patch. - /// The patch for the content item. - /// Indicates whether the patch is a proposal. - /// - /// 200 => Content patched. - /// 404 => Content, schema or app not found. - /// 400 => Content patch is not valid. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpPatch] - [Route("content/{app}/{name}/{id}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission(Permissions.AppContentsUpdate)] - [ApiCosts(1)] - public async Task PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) - { - await contentQuery.GetSchemaOrThrowAsync(Context, name); - - var command = new PatchContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// - /// Publish a content item. - /// - /// The name of the app. - /// The name of the schema. - /// The id of the content item to publish. - /// The status request. - /// - /// 200 => Content published. - /// 404 => Content, schema or app not found. - /// 400 => Request is not valid. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpPut] - [Route("content/{app}/{name}/{id}/status/")] - [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission] - [ApiCosts(1)] - public async Task PutContentStatus(string app, string name, Guid id, ChangeStatusDto request) - { - await contentQuery.GetSchemaOrThrowAsync(Context, name); - - var command = request.ToCommand(id); - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// - /// Discard changes. - /// - /// The name of the app. - /// The name of the schema. - /// The id of the content item to discard changes. - /// - /// 200 => Content restored. - /// 404 => Content, schema or app not found. - /// 400 => Content was not archived. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpPut] - [Route("content/{app}/{name}/{id}/discard/")] - [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission(Permissions.AppContentsDraftDiscard)] - [ApiCosts(1)] - public async Task DiscardDraft(string app, string name, Guid id) - { - await contentQuery.GetSchemaOrThrowAsync(Context, name); - - var command = new DiscardChanges { ContentId = id }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// - /// Delete a content item. - /// - /// The name of the app. - /// The name of the schema. - /// The id of the content item to delete. - /// - /// 204 => Content deleted. - /// 404 => Content, schema or app not found. - /// - /// - /// You can create an generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpDelete] - [Route("content/{app}/{name}/{id}/")] - [ApiPermission(Permissions.AppContentsDelete)] - [ApiCosts(1)] - public async Task DeleteContent(string app, string name, Guid id) - { - await contentQuery.GetSchemaOrThrowAsync(Context, name); - - var command = new DeleteContent { ContentId = id }; - - await CommandBus.PublishAsync(command); - - return NoContent(); - } - - private async Task InvokeCommandAsync(ICommand command) - { - var context = await CommandBus.PublishAsync(command); - - var result = context.Result(); - var response = ContentDto.FromContent(Context, result, this); - - return response; - } - - private bool ShouldProvideSurrogateKeys(IReadOnlyList response) - { - return controllerOptions.EnableSurrogateKeys && response.Count <= controllerOptions.MaxItemsForSurrogateKeys; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs deleted file mode 100644 index 8399e0edd..000000000 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ /dev/null @@ -1,194 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using NodaTime; -using Squidex.Areas.Api.Controllers.Schemas.Models; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Reflection; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Contents.Models -{ - public sealed class ContentDto : Resource - { - /// - /// The if of the content item. - /// - public Guid Id { get; set; } - - /// - /// The user that has created the content item. - /// - [Required] - public RefToken CreatedBy { get; set; } - - /// - /// The user that has updated the content item. - /// - [Required] - public RefToken LastModifiedBy { get; set; } - - /// - /// The data of the content item. - /// - [Required] - public object Data { get; set; } - - /// - /// The pending changes of the content item. - /// - public object DataDraft { get; set; } - - /// - /// The reference data for the frontend UI. - /// - public NamedContentData ReferenceData { get; set; } - - /// - /// Indicates if the draft data is pending. - /// - public bool IsPending { get; set; } - - /// - /// The scheduled status. - /// - public ScheduleJobDto ScheduleJob { get; set; } - - /// - /// The date and time when the content item has been created. - /// - public Instant Created { get; set; } - - /// - /// The date and time when the content item has been modified last. - /// - public Instant LastModified { get; set; } - - /// - /// The status of the content. - /// - public Status Status { get; set; } - - /// - /// The color of the status. - /// - public string StatusColor { get; set; } - - /// - /// The name of the schema. - /// - public string SchemaName { get; set; } - - /// - /// The display name of the schema. - /// - public string SchemaDisplayName { get; set; } - - /// - /// The reference fields. - /// - public FieldDto[] ReferenceFields { get; set; } - - /// - /// The version of the content. - /// - public long Version { get; set; } - - public static ContentDto FromContent(Context context, IEnrichedContentEntity content, ApiController controller) - { - var response = SimpleMapper.Map(content, new ContentDto()); - - if (context.IsFlatten()) - { - response.Data = content.Data?.ToFlatten(); - response.DataDraft = content.DataDraft?.ToFlatten(); - } - else - { - response.Data = content.Data; - response.DataDraft = content.DataDraft; - } - - if (content.ReferenceFields != null) - { - response.ReferenceFields = content.ReferenceFields.Select(FieldDto.FromField).ToArray(); - } - - if (content.ScheduleJob != null) - { - response.ScheduleJob = SimpleMapper.Map(content.ScheduleJob, new ScheduleJobDto()); - } - - return response.CreateLinksAsync(content, controller, content.AppId.Name, content.SchemaId.Name); - } - - private ContentDto CreateLinksAsync(IEnrichedContentEntity content, ApiController controller, string app, string schema) - { - var values = new { app, name = schema, id = Id }; - - AddSelfLink(controller.Url(x => nameof(x.GetContent), values)); - - if (Version > 0) - { - var versioned = new { app, name = schema, id = Id, version = Version - 1 }; - - AddGetLink("prev", controller.Url(x => nameof(x.GetContentVersion), versioned)); - } - - if (IsPending) - { - if (controller.HasPermission(Permissions.AppContentsDraftDiscard, app, schema)) - { - AddPutLink("draft/discard", controller.Url(x => nameof(x.DiscardDraft), values)); - } - - if (controller.HasPermission(Permissions.AppContentsDraftPublish, app, schema)) - { - AddPutLink("draft/publish", controller.Url(x => nameof(x.PutContentStatus), values)); - } - } - - if (controller.HasPermission(Permissions.AppContentsUpdate, app, schema)) - { - if (content.CanUpdate) - { - AddPutLink("update", controller.Url(x => nameof(x.PutContent), values)); - } - - if (Status == Status.Published) - { - AddPutLink("draft/propose", controller.Url((ContentsController x) => nameof(x.PutContent), values) + "?asDraft=true"); - } - - AddPatchLink("patch", controller.Url(x => nameof(x.PatchContent), values)); - - if (content.Nexts != null) - { - foreach (var next in content.Nexts) - { - AddPutLink($"status/{next.Status}", controller.Url(x => nameof(x.PutContentStatus), values), next.Color); - } - } - } - - if (controller.HasPermission(Permissions.AppContentsDelete, app, schema)) - { - AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteContent), values)); - } - - return this; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs deleted file mode 100644 index cd02ddaa7..000000000 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ /dev/null @@ -1,81 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Contents.Models -{ - public sealed class ContentsDto : Resource - { - /// - /// The total number of content items. - /// - public long Total { get; set; } - - /// - /// The content items. - /// - [Required] - public ContentDto[] Items { get; set; } - - /// - /// The possible statuses. - /// - [Required] - public StatusInfoDto[] Statuses { get; set; } - - public static async Task FromContentsAsync(IResultList contents, Context context, ApiController controller, - ISchemaEntity schema, IContentWorkflow workflow) - { - var result = new ContentsDto - { - Total = contents.Total, - Items = contents.Select(x => ContentDto.FromContent(context, x, controller)).ToArray() - }; - - if (schema != null) - { - await result.AssignStatusesAsync(workflow, schema); - - result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name); - } - - return result; - } - - private async Task AssignStatusesAsync(IContentWorkflow workflow, ISchemaEntity schema) - { - var allStatuses = await workflow.GetAllAsync(schema); - - Statuses = allStatuses.Select(StatusInfoDto.FromStatusInfo).ToArray(); - } - - private ContentsDto CreateLinks(ApiController controller, string app, string schema) - { - var values = new { app, name = schema }; - - AddSelfLink(controller.Url(x => nameof(x.GetContents), values)); - - if (controller.HasPermission(Permissions.AppContentsCreate, app, schema)) - { - AddPostLink("create", controller.Url(x => nameof(x.PostContent), values)); - - AddPostLink("create/publish", controller.Url(x => nameof(x.PostContent), values) + "?publish=true"); - } - - return this; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs deleted file mode 100644 index 99de9745b..000000000 --- a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; -using Squidex.Areas.Api.Controllers.Plans.Models; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure.Commands; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Plans -{ - /// - /// Manages and configures plans. - /// - [ApiExplorerSettings(GroupName = nameof(Plans))] - public sealed class AppPlansController : ApiController - { - private readonly IAppPlansProvider appPlansProvider; - private readonly IAppPlanBillingManager appPlansBillingManager; - - public AppPlansController(ICommandBus commandBus, - IAppPlansProvider appPlansProvider, - IAppPlanBillingManager appPlansBillingManager) - : base(commandBus) - { - this.appPlansProvider = appPlansProvider; - this.appPlansBillingManager = appPlansBillingManager; - } - - /// - /// Get app plan information. - /// - /// The name of the app. - /// - /// 200 => App plan information returned. - /// 404 => App not found. - /// - [HttpGet] - [Route("apps/{app}/plans/")] - [ProducesResponseType(typeof(AppPlansDto), 200)] - [ApiPermission(Permissions.AppPlansRead)] - [ApiCosts(0)] - public IActionResult GetPlans(string app) - { - var hasPortal = appPlansBillingManager.HasPortal; - - var response = Deferred.Response(() => - { - return AppPlansDto.FromApp(App, appPlansProvider, hasPortal); - }); - - Response.Headers[HeaderNames.ETag] = App.ToEtag(); - - return Ok(response); - } - - /// - /// Change the app plan. - /// - /// The name of the app. - /// Plan object that needs to be changed. - /// - /// 200 => Plan changed or redirect url returned. - /// 400 => Plan not owned by user. - /// 404 => App not found. - /// - [HttpPut] - [Route("apps/{app}/plan/")] - [ProducesResponseType(typeof(PlanChangedDto), 200)] - [ApiPermission(Permissions.AppPlansChange)] - [ApiCosts(0)] - public async Task PutPlan(string app, [FromBody] ChangePlanDto request) - { - var context = await CommandBus.PublishAsync(request.ToCommand()); - - string redirectUri = null; - - if (context.PlainResult is RedirectToCheckoutResult result) - { - redirectUri = result.Url.ToString(); - } - - return Ok(new PlanChangedDto { RedirectUri = redirectUri }); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs b/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs deleted file mode 100644 index 90d995e57..000000000 --- a/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.ComponentModel.DataAnnotations; -using System.Linq; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Services; - -namespace Squidex.Areas.Api.Controllers.Plans.Models -{ - public sealed class AppPlansDto - { - /// - /// The available plans. - /// - [Required] - public PlanDto[] Plans { get; set; } - - /// - /// The current plan id. - /// - public string CurrentPlanId { get; set; } - - /// - /// The plan owner. - /// - public string PlanOwner { get; set; } - - /// - /// Indicates if there is a billing portal. - /// - public bool HasPortal { get; set; } - - public static AppPlansDto FromApp(IAppEntity app, IAppPlansProvider plans, bool hasPortal) - { - var planId = app.Plan?.PlanId; - - var response = new AppPlansDto - { - CurrentPlanId = planId, - Plans = plans.GetAvailablePlans().Select(PlanDto.FromPlan).ToArray(), - PlanOwner = app.Plan?.Owner.Identifier, - HasPortal = hasPortal - }; - - return response; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs b/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs deleted file mode 100644 index a35510786..000000000 --- a/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Areas.Api.Controllers.Plans.Models -{ - public sealed class PlanChangedDto - { - /// - /// Optional redirect uri. - /// - public string RedirectUri { get; set; } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs deleted file mode 100644 index a39b15600..000000000 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs +++ /dev/null @@ -1,78 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using Namotion.Reflection; -using NJsonSchema; -using NSwag.Generation.Processors; -using NSwag.Generation.Processors.Contexts; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Infrastructure; - -namespace Squidex.Areas.Api.Controllers.Rules.Models -{ - public sealed class RuleActionProcessor : IDocumentProcessor - { - private readonly RuleRegistry ruleRegistry; - - public RuleActionProcessor(RuleRegistry ruleRegistry) - { - Guard.NotNull(ruleRegistry, nameof(ruleRegistry)); - - this.ruleRegistry = ruleRegistry; - } - - public void Process(DocumentProcessorContext context) - { - try - { - var schema = context.SchemaResolver.GetSchema(typeof(RuleAction), false); - - if (schema != null) - { - schema.DiscriminatorObject = new OpenApiDiscriminator - { - JsonInheritanceConverter = new RuleActionConverter(), PropertyName = "actionType" - }; - - schema.Properties["actionType"] = new JsonSchemaProperty - { - Type = JsonObjectType.String, IsRequired = true - }; - - foreach (var (key, value) in ruleRegistry.Actions) - { - var derivedSchema = context.SchemaGenerator.Generate(value.Type.ToContextualType(), context.SchemaResolver); - - var oldName = context.Document.Definitions.FirstOrDefault(x => x.Value == derivedSchema).Key; - - if (oldName != null) - { - context.Document.Definitions.Remove(oldName); - context.Document.Definitions.Add($"{key}RuleActionDto", derivedSchema); - } - } - - RemoveFreezable(context, schema); - } - } - catch (KeyNotFoundException) - { - return; - } - } - - private static void RemoveFreezable(DocumentProcessorContext context, JsonSchema schema) - { - context.Document.Definitions.Remove("Freezable"); - - schema.AllOf.Clear(); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs deleted file mode 100644 index 6c7785e60..000000000 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.ObjectModel; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Runtime.Serialization; -using Newtonsoft.Json; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Web.Json; - -namespace Squidex.Areas.Api.Controllers.Schemas.Models -{ - [JsonConverter(typeof(TypedJsonInheritanceConverter), "fieldType")] - [KnownType(nameof(Subtypes))] - public abstract class FieldPropertiesDto - { - /// - /// Optional label for the editor. - /// - [StringLength(100)] - public string Label { get; set; } - - /// - /// Hints to describe the schema. - /// - [StringLength(1000)] - public string Hints { get; set; } - - /// - /// Placeholder to show when no value has been entered. - /// - [StringLength(100)] - public string Placeholder { get; set; } - - /// - /// Indicates if the field is required. - /// - public bool IsRequired { get; set; } - - /// - /// Determines if the field should be displayed in lists. - /// - public bool IsListField { get; set; } - - /// - /// Determines if the field should be displayed in reference lists. - /// - public bool IsReferenceField { get; set; } - - /// - /// Optional url to the editor. - /// - public string EditorUrl { get; set; } - - /// - /// Tags for automation processes. - /// - public ReadOnlyCollection Tags { get; set; } - - public abstract FieldProperties ToProperties(); - - public static Type[] Subtypes() - { - var type = typeof(FieldPropertiesDto); - - return type.Assembly.GetTypes().Where(type.IsAssignableFrom).ToArray(); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs deleted file mode 100644 index 0a55fe8e9..000000000 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.ComponentModel.DataAnnotations; -using Squidex.Domain.Apps.Entities.Schemas.Commands; - -namespace Squidex.Areas.Api.Controllers.Schemas.Models -{ - public sealed class UpdateFieldDto - { - /// - /// The field properties. - /// - [Required] - public FieldPropertiesDto Properties { get; set; } - - public UpdateField ToCommand(long id, long? parentId = null) - { - return new UpdateField { ParentFieldId = parentId, FieldId = id, Properties = Properties?.ToProperties() }; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs deleted file mode 100644 index 63ba803ec..000000000 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Areas.Api.Controllers.Schemas.Models -{ - public abstract class UpsertSchemaDto - { - /// - /// The optional properties. - /// - public SchemaPropertiesDto Properties { get; set; } - - /// - /// The optional scripts. - /// - public SchemaScriptsDto Scripts { get; set; } - - /// - /// Optional fields. - /// - public List Fields { get; set; } - - /// - /// The optional preview urls. - /// - public Dictionary PreviewUrls { get; set; } - - /// - /// The category. - /// - public string Category { get; set; } - - /// - /// Set it to true to autopublish the schema. - /// - public bool IsPublished { get; set; } - - public static TCommand ToCommand(TDto dto, TCommand command) where TCommand : UpsertCommand where TDto : UpsertSchemaDto - { - SimpleMapper.Map(dto, command); - - if (dto.Properties != null) - { - command.Properties = new SchemaProperties(); - - SimpleMapper.Map(dto.Properties, command.Properties); - } - - if (dto.Scripts != null) - { - command.Scripts = new SchemaScripts(); - - SimpleMapper.Map(dto.Scripts, command.Scripts); - } - - if (dto.Fields != null) - { - command.Fields = new List(); - - foreach (var rootFieldDto in dto.Fields) - { - var rootProps = rootFieldDto?.Properties?.ToProperties(); - var rootField = new UpsertSchemaField { Properties = rootProps }; - - SimpleMapper.Map(rootFieldDto, rootField); - - if (rootFieldDto?.Nested?.Count > 0) - { - rootField.Nested = new List(); - - foreach (var nestedFieldDto in rootFieldDto.Nested) - { - var nestedProps = nestedFieldDto?.Properties?.ToProperties(); - var nestedField = new UpsertSchemaNestedField { Properties = nestedProps }; - - SimpleMapper.Map(nestedFieldDto, nestedField); - - rootField.Nested.Add(nestedField); - } - } - - command.Fields.Add(rootField); - } - } - - return command; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs deleted file mode 100644 index ea82636b5..000000000 --- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ /dev/null @@ -1,330 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; -using Squidex.Areas.Api.Controllers.Schemas.Models; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure.Commands; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Schemas -{ - /// - /// Manages and retrieves information about schemas. - /// - [ApiExplorerSettings(GroupName = nameof(Schemas))] - public sealed class SchemasController : ApiController - { - private readonly IAppProvider appProvider; - - public SchemasController(ICommandBus commandBus, IAppProvider appProvider) - : base(commandBus) - { - this.appProvider = appProvider; - } - - /// - /// Get schemas. - /// - /// The name of the app. - /// - /// 200 => Schemas returned. - /// 404 => App not found. - /// - [HttpGet] - [Route("apps/{app}/schemas/")] - [ProducesResponseType(typeof(SchemasDto), 200)] - [ApiPermission(Permissions.AppCommon)] - [ApiCosts(0)] - public async Task GetSchemas(string app) - { - var schemas = await appProvider.GetSchemasAsync(AppId); - - var response = Deferred.Response(() => - { - return SchemasDto.FromSchemas(schemas, this, app); - }); - - Response.Headers[HeaderNames.ETag] = schemas.ToEtag(); - - return Ok(response); - } - - /// - /// Get a schema by name. - /// - /// The name of the app. - /// The name of the schema to retrieve. - /// - /// 200 => Schema found. - /// 404 => Schema or app not found. - /// - [HttpGet] - [Route("apps/{app}/schemas/{name}/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppCommon)] - [ApiCosts(0)] - public async Task GetSchema(string app, string name) - { - ISchemaEntity schema; - - if (Guid.TryParse(name, out var id)) - { - schema = await appProvider.GetSchemaAsync(AppId, id); - } - else - { - schema = await appProvider.GetSchemaAsync(AppId, name); - } - - if (schema == null || schema.IsDeleted) - { - return NotFound(); - } - - var response = Deferred.Response(() => - { - return SchemaDetailsDto.FromSchemaWithDetails(schema, this, app); - }); - - Response.Headers[HeaderNames.ETag] = schema.ToEtag(); - - return Ok(response); - } - - /// - /// Create a new schema. - /// - /// The name of the app. - /// The schema object that needs to be added to the app. - /// - /// 201 => Schema created. - /// 400 => Schema name or properties are not valid. - /// 409 => Schema name already in use. - /// - [HttpPost] - [Route("apps/{app}/schemas/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 201)] - [ApiPermission(Permissions.AppSchemasCreate)] - [ApiCosts(1)] - public async Task PostSchema(string app, [FromBody] CreateSchemaDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(app, command); - - return CreatedAtAction(nameof(GetSchema), new { app, name = request.Name }, response); - } - - /// - /// Update a schema. - /// - /// The name of the app. - /// The name of the schema. - /// The schema object that needs to updated. - /// - /// 200 => Schema updated. - /// 400 => Schema properties are not valid. - /// 404 => Schema or app not found. - /// - [HttpPut] - [Route("apps/{app}/schemas/{name}/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task PutSchema(string app, string name, [FromBody] UpdateSchemaDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Synchronize a schema. - /// - /// The name of the app. - /// The name of the schema. - /// The schema object that needs to updated. - /// - /// 200 => Schema updated. - /// 400 => Schema properties are not valid. - /// 404 => Schema or app not found. - /// - [HttpPut] - [Route("apps/{app}/schemas/{name}/sync")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task PutSchemaSync(string app, string name, [FromBody] SynchronizeSchemaDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Update a schema category. - /// - /// The name of the app. - /// The name of the schema. - /// The schema object that needs to updated. - /// - /// 200 => Schema updated. - /// 404 => Schema or app not found. - /// - [HttpPut] - [Route("apps/{app}/schemas/{name}/category")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task PutCategory(string app, string name, [FromBody] ChangeCategoryDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Update the preview urls. - /// - /// The name of the app. - /// The name of the schema. - /// The preview urls for the schema. - /// - /// 200 => Schema updated. - /// 404 => Schema or app not found. - /// - [HttpPut] - [Route("apps/{app}/schemas/{name}/preview-urls")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task PutPreviewUrls(string app, string name, [FromBody] ConfigurePreviewUrlsDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Update the scripts. - /// - /// The name of the app. - /// The name of the schema. - /// The schema scripts object that needs to updated. - /// - /// 200 => Schema updated. - /// 400 => Schema properties are not valid. - /// 404 => Schema or app not found. - /// - [HttpPut] - [Route("apps/{app}/schemas/{name}/scripts/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasScripts)] - [ApiCosts(1)] - public async Task PutScripts(string app, string name, [FromBody] SchemaScriptsDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Publish a schema. - /// - /// The name of the app. - /// The name of the schema to publish. - /// - /// 200 => Schema has been published. - /// 400 => Schema is already published. - /// 404 => Schema or app not found. - /// - [HttpPut] - [Route("apps/{app}/schemas/{name}/publish/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasPublish)] - [ApiCosts(1)] - public async Task PublishSchema(string app, string name) - { - var command = new PublishSchema(); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Unpublish a schema. - /// - /// The name of the app. - /// The name of the schema to unpublish. - /// - /// 200 => Schema has been unpublished. - /// 400 => Schema is not published. - /// 404 => Schema or app not found. - /// - [HttpPut] - [Route("apps/{app}/schemas/{name}/unpublish/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasPublish)] - [ApiCosts(1)] - public async Task UnpublishSchema(string app, string name) - { - var command = new UnpublishSchema(); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Delete a schema. - /// - /// The name of the app. - /// The name of the schema to delete. - /// - /// 204 => Schema deleted. - /// 404 => Schema or app not found. - /// - [HttpDelete] - [Route("apps/{app}/schemas/{name}/")] - [ApiPermission(Permissions.AppSchemasDelete)] - [ApiCosts(1)] - public async Task DeleteSchema(string app, string name) - { - await CommandBus.PublishAsync(new DeleteSchema()); - - return NoContent(); - } - - private async Task InvokeCommandAsync(string app, ICommand command) - { - var context = await CommandBus.PublishAsync(command); - - var result = context.Result(); - var response = SchemaDetailsDto.FromSchemaWithDetails(result, this, app); - - return response; - } - } -} \ No newline at end of file diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs deleted file mode 100644 index 81260dd3f..000000000 --- a/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs +++ /dev/null @@ -1,95 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using Squidex.Infrastructure.Reflection; -using Squidex.Shared.Users; -using Squidex.Web; -using AllPermissions = Squidex.Shared.Permissions; - -namespace Squidex.Areas.Api.Controllers.Users.Models -{ - public sealed class UserDto : Resource - { - /// - /// The id of the user. - /// - [Required] - public string Id { get; set; } - - /// - /// The email of the user. Unique value. - /// - [Required] - public string Email { get; set; } - - /// - /// The display name (usually first name and last name) of the user. - /// - [Required] - public string DisplayName { get; set; } - - /// - /// Determines if the user is locked. - /// - [Required] - public bool IsLocked { get; set; } - - /// - /// Additional permissions for the user. - /// - [Required] - public IEnumerable Permissions { get; set; } - - public static UserDto FromUser(IUser user, ApiController controller) - { - var userPermssions = user.Permissions().ToIds(); - var userName = user.DisplayName(); - - var result = SimpleMapper.Map(user, new UserDto { DisplayName = userName, Permissions = userPermssions }); - - return result.CreateLinks(controller); - } - - private UserDto CreateLinks(ApiController controller) - { - var values = new { id = Id }; - - if (controller is UserManagementController) - { - AddSelfLink(controller.Url(c => nameof(c.GetUser), values)); - } - else - { - AddSelfLink(controller.Url(c => nameof(c.GetUser), values)); - } - - if (!controller.IsUser(Id)) - { - if (controller.HasPermission(AllPermissions.AdminUsersLock) && !IsLocked) - { - AddPutLink("lock", controller.Url(c => nameof(c.LockUser), values)); - } - - if (controller.HasPermission(AllPermissions.AdminUsersUnlock) && IsLocked) - { - AddPutLink("unlock", controller.Url(c => nameof(c.UnlockUser), values)); - } - } - - if (controller.HasPermission(AllPermissions.AdminUsersUpdate)) - { - AddPutLink("update", controller.Url(c => nameof(c.PutUser), values)); - } - - AddGetLink("picture", controller.Url(c => nameof(c.GetUserPicture), values)); - - return this; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs deleted file mode 100644 index 9167ef148..000000000 --- a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs +++ /dev/null @@ -1,129 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Squidex.Areas.Api.Controllers.Users.Models; -using Squidex.Domain.Users; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Validation; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Users -{ - [ApiModelValidation(true)] - public sealed class UserManagementController : ApiController - { - private readonly UserManager userManager; - private readonly IUserFactory userFactory; - - public UserManagementController(ICommandBus commandBus, UserManager userManager, IUserFactory userFactory) - : base(commandBus) - { - this.userManager = userManager; - this.userFactory = userFactory; - } - - [HttpGet] - [Route("user-management/")] - [ProducesResponseType(typeof(UsersDto), 200)] - [ApiPermission(Permissions.AdminUsersRead)] - public async Task GetUsers([FromQuery] string query = null, [FromQuery] int skip = 0, [FromQuery] int take = 10) - { - var taskForItems = userManager.QueryByEmailAsync(query, take, skip); - var taskForCount = userManager.CountByEmailAsync(query); - - await Task.WhenAll(taskForItems, taskForCount); - - var response = UsersDto.FromResults(taskForItems.Result, taskForCount.Result, this); - - return Ok(response); - } - - [HttpGet] - [Route("user-management/{id}/")] - [ProducesResponseType(typeof(UserDto), 201)] - [ApiPermission(Permissions.AdminUsersRead)] - public async Task GetUser(string id) - { - var user = await userManager.FindByIdWithClaimsAsync(id); - - if (user == null) - { - return NotFound(); - } - - var response = UserDto.FromUser(user, this); - - return Ok(response); - } - - [HttpPost] - [Route("user-management/")] - [ProducesResponseType(typeof(UserDto), 201)] - [ApiPermission(Permissions.AdminUsersCreate)] - public async Task PostUser([FromBody] CreateUserDto request) - { - var user = await userManager.CreateAsync(userFactory, request.ToValues()); - - var response = UserDto.FromUser(user, this); - - return Ok(response); - } - - [HttpPut] - [Route("user-management/{id}/")] - [ProducesResponseType(typeof(UserDto), 201)] - [ApiPermission(Permissions.AdminUsersUpdate)] - public async Task PutUser(string id, [FromBody] UpdateUserDto request) - { - var user = await userManager.UpdateAsync(id, request.ToValues()); - - var response = UserDto.FromUser(user, this); - - return Ok(response); - } - - [HttpPut] - [Route("user-management/{id}/lock/")] - [ProducesResponseType(typeof(UserDto), 201)] - [ApiPermission(Permissions.AdminUsersLock)] - public async Task LockUser(string id) - { - if (this.IsUser(id)) - { - throw new ValidationException("Locking user failed.", new ValidationError("You cannot lock yourself.")); - } - - var user = await userManager.LockAsync(id); - - var response = UserDto.FromUser(user, this); - - return Ok(response); - } - - [HttpPut] - [Route("user-management/{id}/unlock/")] - [ProducesResponseType(typeof(UserDto), 201)] - [ApiPermission(Permissions.AdminUsersUnlock)] - public async Task UnlockUser(string id) - { - if (this.IsUser(id)) - { - throw new ValidationException("Unlocking user failed.", new ValidationError("You cannot unlock yourself.")); - } - - var user = await userManager.UnlockAsync(id); - - var response = UserDto.FromUser(user, this); - - return Ok(response); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs deleted file mode 100644 index 597dc64de..000000000 --- a/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs +++ /dev/null @@ -1,198 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Squidex.Areas.Api.Controllers.Users.Models; -using Squidex.Domain.Users; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Shared.Users; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Users -{ - /// - /// Readonly API to retrieve information about squidex users. - /// - [ApiExplorerSettings(GroupName = nameof(Users))] - public sealed class UsersController : ApiController - { - private static readonly byte[] AvatarBytes; - private readonly IUserPictureStore userPictureStore; - private readonly IUserResolver userResolver; - private readonly ISemanticLog log; - - static UsersController() - { - var assembly = typeof(UsersController).Assembly; - - using (var avatarStream = assembly.GetManifestResourceStream("Squidex.Areas.Api.Controllers.Users.Assets.Avatar.png")) - { - AvatarBytes = new byte[avatarStream.Length]; - - avatarStream.Read(AvatarBytes, 0, AvatarBytes.Length); - } - } - - public UsersController( - ICommandBus commandBus, - IUserPictureStore userPictureStore, - IUserResolver userResolver, - ISemanticLog log) - : base(commandBus) - { - this.userPictureStore = userPictureStore; - this.userResolver = userResolver; - - this.log = log; - } - - /// - /// Get the user resources. - /// - /// - /// 200 => User resources returned. - /// - [HttpGet] - [Route("/")] - [ProducesResponseType(typeof(ResourcesDto), 200)] - [ApiPermission] - public IActionResult GetUserResources() - { - var response = ResourcesDto.FromController(this); - - return Ok(response); - } - - /// - /// Get users by query. - /// - /// The query to search the user by email address. Case invariant. - /// - /// Search the user by query that contains the email address or the part of the email address. - /// - /// - /// 200 => Users returned. - /// - [HttpGet] - [Route("users/")] - [ProducesResponseType(typeof(UserDto[]), 200)] - [ApiPermission] - public async Task GetUsers(string query) - { - try - { - var users = await userResolver.QueryByEmailAsync(query); - - var response = users.Where(x => !x.IsHidden()).Select(x => UserDto.FromUser(x, this)).ToArray(); - - return Ok(response); - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", nameof(GetUsers)) - .WriteProperty("status", "Failed")); - } - - return Ok(new UserDto[0]); - } - - /// - /// Get user by id. - /// - /// The id of the user (GUID). - /// - /// 200 => User found. - /// 404 => User not found. - /// - [HttpGet] - [Route("users/{id}/")] - [ProducesResponseType(typeof(UserDto), 200)] - [ApiPermission] - public async Task GetUser(string id) - { - try - { - var entity = await userResolver.FindByIdOrEmailAsync(id); - - if (entity != null) - { - var response = UserDto.FromUser(entity, this); - - return Ok(response); - } - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", nameof(GetUser)) - .WriteProperty("status", "Failed")); - } - - return NotFound(); - } - - /// - /// Get user picture by id. - /// - /// The id of the user (GUID). - /// - /// 200 => User found and image or fallback returned. - /// 404 => User not found. - /// - [HttpGet] - [Route("users/{id}/picture/")] - [ProducesResponseType(typeof(FileResult), 200)] - [ResponseCache(Duration = 300)] - public async Task GetUserPicture(string id) - { - try - { - var entity = await userResolver.FindByIdOrEmailAsync(id); - - if (entity != null) - { - if (entity.IsPictureUrlStored()) - { - return new FileStreamResult(await userPictureStore.DownloadAsync(entity.Id), "image/png"); - } - - using (var client = new HttpClient()) - { - var url = entity.PictureNormalizedUrl(); - - if (!string.IsNullOrWhiteSpace(url)) - { - var response = await client.GetAsync(url); - - if (response.IsSuccessStatusCode) - { - var contentType = response.Content.Headers.ContentType.ToString(); - - return new FileStreamResult(await response.Content.ReadAsStreamAsync(), contentType); - } - } - } - } - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", nameof(GetUser)) - .WriteProperty("status", "Failed")); - } - - return new FileStreamResult(new MemoryStream(AvatarBytes), "image/png"); - } - } -} diff --git a/src/Squidex/Areas/Api/Startup.cs b/src/Squidex/Areas/Api/Startup.cs deleted file mode 100644 index d5fb554b2..000000000 --- a/src/Squidex/Areas/Api/Startup.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Builder; -using Squidex.Areas.Api.Config.OpenApi; -using Squidex.Web; - -namespace Squidex.Areas.Api -{ - public static class Startup - { - public static void ConfigureApi(this IApplicationBuilder app) - { - app.Map(Constants.ApiPrefix, appApi => - { - appApi.UseMyOpenApi(); - appApi.UseMvc(); - }); - } - } -} diff --git a/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs b/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs deleted file mode 100644 index 8ed930af6..000000000 --- a/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.IO; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace Squidex.Areas.Frontend.Middlewares -{ - public sealed class WebpackMiddleware - { - private const string WebpackUrl = "http://localhost:3000/index.html"; - private readonly RequestDelegate next; - - public WebpackMiddleware(RequestDelegate next) - { - this.next = next; - } - - public async Task Invoke(HttpContext context) - { - if (context.IsIndex() && context.Response.StatusCode != 304) - { - using (var client = new HttpClient()) - { - var result = await client.GetAsync(WebpackUrl); - - context.Response.StatusCode = (int)result.StatusCode; - - if (result.IsSuccessStatusCode) - { - var html = await result.Content.ReadAsStringAsync(); - - html = html.AdjustHtml(context); - - await context.Response.WriteHtmlAsync(html); - } - } - } - else if (context.IsHtmlPath() && context.Response.StatusCode != 304) - { - var responseBuffer = new MemoryStream(); - var responseBody = context.Response.Body; - - context.Response.Body = responseBuffer; - - await next(context); - - if (context.Response.StatusCode != 304) - { - context.Response.Body = responseBody; - - var html = Encoding.UTF8.GetString(responseBuffer.ToArray()); - - html = html.AdjustHtml(context); - - context.Response.ContentLength = Encoding.UTF8.GetByteCount(html); - context.Response.Body = responseBody; - - await context.Response.WriteAsync(html); - } - } - else - { - await next(context); - } - } - } -} diff --git a/src/Squidex/Areas/Frontend/Startup.cs b/src/Squidex/Areas/Frontend/Startup.cs deleted file mode 100644 index 875945463..000000000 --- a/src/Squidex/Areas/Frontend/Startup.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Net.Http.Headers; -using Squidex.Areas.Frontend.Middlewares; -using Squidex.Pipeline.Squid; - -namespace Squidex.Areas.Frontend -{ - public static class Startup - { - public static void ConfigureFrontend(this IApplicationBuilder app) - { - var environment = app.ApplicationServices.GetRequiredService(); - - app.UseMiddleware(); - - app.Use((context, next) => - { - if (context.Request.Path == "/client-callback-popup") - { - context.Request.Path = new PathString("/client-callback-popup.html"); - } - else if (context.Request.Path == "/client-callback-silent") - { - context.Request.Path = new PathString("/client-callback-silent.html"); - } - else if (!Path.HasExtension(context.Request.Path.Value)) - { - if (environment.IsDevelopment()) - { - context.Request.Path = new PathString("/index.html"); - } - else - { - context.Request.Path = new PathString("/build/index.html"); - } - } - - return next(); - }); - - if (environment.IsDevelopment()) - { - app.UseMiddleware(); - } - else - { - app.UseMiddleware(); - } - - app.UseStaticFiles(new StaticFileOptions - { - OnPrepareResponse = context => - { - var response = context.Context.Response; - var responseHeaders = response.GetTypedHeaders(); - - if (!string.Equals(response.ContentType, "text/html", StringComparison.OrdinalIgnoreCase)) - { - responseHeaders.CacheControl = new CacheControlHeaderValue - { - MaxAge = TimeSpan.FromDays(60) - }; - } - else - { - responseHeaders.CacheControl = new CacheControlHeaderValue - { - NoCache = true - }; - } - } - }); - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.pfx b/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.pfx deleted file mode 100644 index 7a70ee610be6df1871798d3f87c0f6a5f62ca605..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2573 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 diff --git a/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.snk b/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.snk deleted file mode 100644 index 252e41c3dc68437337b95f9053b3d208e0360b31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50098o7t)9uStUbz@gZ^jH8@=58BGYP8sFC~ zE~l92$unDc4r5`&CN13WKZ@ghG-}Ha0@6oVS{!`P5zI~i5cKk*P3N9-L`KYNhDhjO z=g+a&py{BFQMI(JO9|M^XOU3AU<(y+S6^wLB25!~>KDUlhF8&ob!qVt+-m)o^>@?x zhb*w7TFCAUQwJ9>O#N${DYNTcc1=Z@H|I<2+|kIE93dBD0RjxgqRu*zC6v1!09o1b zv!2Qxz}Gr=|Hkce+c!p8u_ctT_7C}F;Wb9L6~2-}_y&s7C0dAG*3=>d{oV(w4W_Lu zd;*Mh<4cvo#`2;D{pT7hznBpS&aC4p73mOo=si+{)^cdX>aw-#%6DNk9G*{Gk9c$6 ziQL{dvzOH(hxbi%`Nj)lC)Y67e$>lEFu`~&dyE}oIy6HigrzY~{8(KD!ijpYDk~>~a1C+WX7{`|soqfOb5-W-;*Qbe$nOVh zM?+>fimT8=sP*vvZn5pB`R2>Y%{xe{_6l8|be%V~cEgg{wIlW}Hx1dQ6B(IiK<6-6 z7<{BpDaSU6_veQe)@c00;zOFD9Uf^#KHO84GL)9jr|FU|vLp_$E6WEyi1R9-_g3p# z$&6Mq&IdHog;N7%U-mfFAcXPP97w^@!xqK8$_^c5@-1!;dO4L^PM2O;7bOL>().Value; - - IdentityModelEventSource.ShowPII = options.ShowPII; - - var userManager = services.GetRequiredService>(); - var userFactory = services.GetRequiredService(); - - var log = services.GetRequiredService(); - - if (options.IsAdminConfigured()) - { - var adminEmail = options.AdminEmail; - var adminPass = options.AdminPassword; - - Task.Run(async () => - { - if (userManager.SupportsQueryableUsers && !userManager.Users.Any()) - { - try - { - var values = new UserValues - { - Email = adminEmail, - Password = adminPass, - Permissions = new PermissionSet(Permissions.Admin), - DisplayName = adminEmail - }; - - await userManager.CreateAsync(userFactory, values); - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "createAdmin") - .WriteProperty("status", "failed")); - } - } - }).Wait(); - } - - return services; - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs b/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs deleted file mode 100644 index 8ede8a1bb..000000000 --- a/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Security.Cryptography.X509Certificates; -using IdentityModel; -using IdentityServer4.Models; -using IdentityServer4.Stores; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.DataProtection.KeyManagement; -using Microsoft.AspNetCore.DataProtection.Repositories; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Squidex.Domain.Users; -using Squidex.Shared.Identity; -using Squidex.Web; - -namespace Squidex.Areas.IdentityServer.Config -{ - public static class IdentityServerServices - { - public static void AddMyIdentityServer(this IServiceCollection services) - { - X509Certificate2 certificate; - - var assembly = typeof(IdentityServerServices).Assembly; - - using (var certStream = assembly.GetManifestResourceStream("Squidex.Areas.IdentityServer.Config.Cert.IdentityCert.pfx")) - { - var certData = new byte[certStream.Length]; - - certStream.Read(certData, 0, certData.Length); - certificate = new X509Certificate2(certData, "password", - X509KeyStorageFlags.MachineKeySet | - X509KeyStorageFlags.PersistKeySet | - X509KeyStorageFlags.Exportable); - } - - services.AddSingleton>(s => - { - return new ConfigureOptions(options => - { - options.XmlRepository = s.GetRequiredService(); - }); - }); - - services.AddDataProtection().SetApplicationName("Squidex"); - services.AddSingleton(GetApiResources()); - services.AddSingleton(GetIdentityResources()); - - services.AddIdentity() - .AddDefaultTokenProviders(); - services.AddSingleton, - PwnedPasswordValidator>(); - services.AddSingleton, - UserClaimsPrincipalFactoryWithEmail>(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddIdentityServer(options => - { - options.UserInteraction.ErrorUrl = "/error/"; - }) - .AddAspNetIdentity() - .AddInMemoryApiResources(GetApiResources()) - .AddInMemoryIdentityResources(GetIdentityResources()) - .AddSigningCredential(certificate); - } - - private static IEnumerable GetApiResources() - { - yield return new ApiResource(Constants.ApiScope) - { - UserClaims = new List - { - JwtClaimTypes.Email, - JwtClaimTypes.Role, - SquidexClaimTypes.Permissions - } - }; - } - - private static IEnumerable GetIdentityResources() - { - yield return new IdentityResources.OpenId(); - yield return new IdentityResources.Profile(); - yield return new IdentityResources.Email(); - yield return new IdentityResource(Constants.RoleScope, - new[] - { - JwtClaimTypes.Role - }); - yield return new IdentityResource(Constants.PermissionsScope, - new[] - { - SquidexClaimTypes.Permissions - }); - yield return new IdentityResource(Constants.ProfileScope, - new[] - { - SquidexClaimTypes.DisplayName, - SquidexClaimTypes.PictureUrl - }); - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs deleted file mode 100644 index 33d60dc0d..000000000 --- a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs +++ /dev/null @@ -1,232 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; -using IdentityServer4; -using IdentityServer4.Models; -using IdentityServer4.Stores; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using Squidex.Config; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Users; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Shared; -using Squidex.Shared.Identity; -using Squidex.Shared.Users; -using Squidex.Web; - -namespace Squidex.Areas.IdentityServer.Config -{ - public class LazyClientStore : IClientStore - { - private readonly UserManager userManager; - private readonly IAppProvider appProvider; - private readonly Dictionary staticClients = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public LazyClientStore( - UserManager userManager, - IOptions urlsOptions, - IOptions identityOptions, - IAppProvider appProvider) - { - Guard.NotNull(identityOptions, nameof(identityOptions)); - Guard.NotNull(urlsOptions, nameof(urlsOptions)); - Guard.NotNull(userManager, nameof(userManager)); - Guard.NotNull(appProvider, nameof(appProvider)); - - this.userManager = userManager; - this.appProvider = appProvider; - - CreateStaticClients(urlsOptions, identityOptions); - } - - public async Task FindClientByIdAsync(string clientId) - { - var client = staticClients.GetOrDefault(clientId); - - if (client != null) - { - return client; - } - - var (appName, appClientId) = clientId.GetClientParts(); - - if (!string.IsNullOrWhiteSpace(appName)) - { - var app = await appProvider.GetAppAsync(appName); - - var appClient = app?.Clients.GetOrDefault(appClientId); - - if (appClient != null) - { - return CreateClientFromApp(clientId, appClient); - } - } - - var user = await userManager.FindByIdWithClaimsAsync(clientId); - - if (!string.IsNullOrWhiteSpace(user?.ClientSecret())) - { - return CreateClientFromUser(user); - } - - return null; - } - - private static Client CreateClientFromUser(UserWithClaims user) - { - return new Client - { - ClientId = user.Id, - ClientName = $"{user.Email} Client", - ClientClaimsPrefix = null, - ClientSecrets = new List - { - new Secret(user.ClientSecret().Sha256()) - }, - AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, - AllowedGrantTypes = GrantTypes.ClientCredentials, - AllowedScopes = new List - { - Constants.ApiScope, - Constants.RoleScope, - Constants.PermissionsScope - }, - Claims = new List - { - new Claim(OpenIdClaims.Subject, user.Id) - } - }; - } - - private static Client CreateClientFromApp(string id, AppClient appClient) - { - return new Client - { - ClientId = id, - ClientName = id, - ClientSecrets = new List - { - new Secret(appClient.Secret.Sha256()) - }, - AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, - AllowedGrantTypes = GrantTypes.ClientCredentials, - AllowedScopes = new List - { - Constants.ApiScope, - Constants.RoleScope, - Constants.PermissionsScope - } - }; - } - - private void CreateStaticClients(IOptions urlsOptions, IOptions identityOptions) - { - foreach (var client in CreateStaticClients(urlsOptions.Value, identityOptions.Value)) - { - staticClients[client.ClientId] = client; - } - } - - private static IEnumerable CreateStaticClients(UrlsOptions urlsOptions, MyIdentityOptions identityOptions) - { - var frontendId = Constants.FrontendClient; - - yield return new Client - { - ClientId = frontendId, - ClientName = frontendId, - RedirectUris = new List - { - urlsOptions.BuildUrl("login;"), - urlsOptions.BuildUrl("client-callback-silent", false), - urlsOptions.BuildUrl("client-callback-popup", false) - }, - PostLogoutRedirectUris = new List - { - urlsOptions.BuildUrl("logout", false) - }, - AllowAccessTokensViaBrowser = true, - AllowedGrantTypes = GrantTypes.Implicit, - AllowedScopes = new List - { - IdentityServerConstants.StandardScopes.OpenId, - IdentityServerConstants.StandardScopes.Profile, - IdentityServerConstants.StandardScopes.Email, - Constants.ApiScope, - Constants.PermissionsScope, - Constants.ProfileScope, - Constants.RoleScope - }, - RequireConsent = false - }; - - var internalClient = Constants.InternalClientId; - - yield return new Client - { - ClientId = internalClient, - ClientName = internalClient, - ClientSecrets = new List - { - new Secret(Constants.InternalClientSecret) - }, - RedirectUris = new List - { - urlsOptions.BuildUrl($"{Constants.PortalPrefix}/signin-internal", false), - urlsOptions.BuildUrl($"{Constants.OrleansPrefix}/signin-internal", false) - }, - AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, - AllowedGrantTypes = GrantTypes.ImplicitAndClientCredentials, - AllowedScopes = new List - { - IdentityServerConstants.StandardScopes.OpenId, - IdentityServerConstants.StandardScopes.Profile, - IdentityServerConstants.StandardScopes.Email, - Constants.ApiScope, - Constants.PermissionsScope, - Constants.ProfileScope, - Constants.RoleScope - }, - RequireConsent = false - }; - - if (identityOptions.IsAdminClientConfigured()) - { - var id = identityOptions.AdminClientId; - - yield return new Client - { - ClientId = id, - ClientName = id, - ClientSecrets = new List - { - new Secret(identityOptions.AdminClientSecret.Sha256()) - }, - AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, - AllowedGrantTypes = GrantTypes.ClientCredentials, - AllowedScopes = new List - { - Constants.ApiScope, - Constants.RoleScope, - Constants.PermissionsScope - }, - Claims = new List - { - new Claim(SquidexClaimTypes.Permissions, Permissions.All) - } - }; - } - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs deleted file mode 100644 index fb645e1a3..000000000 --- a/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs +++ /dev/null @@ -1,433 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Security; -using System.Security.Claims; -using System.Text; -using System.Threading.Tasks; -using IdentityServer4.Models; -using IdentityServer4.Services; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Squidex.Config; -using Squidex.Domain.Users; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; -using Squidex.Shared; -using Squidex.Shared.Identity; -using Squidex.Shared.Users; -using Squidex.Web; - -namespace Squidex.Areas.IdentityServer.Controllers.Account -{ - public sealed class AccountController : IdentityServerController - { - private readonly SignInManager signInManager; - private readonly UserManager userManager; - private readonly IUserFactory userFactory; - private readonly IUserEvents userEvents; - private readonly MyIdentityOptions identityOptions; - private readonly ISemanticLog log; - private readonly IIdentityServerInteractionService interactions; - - public AccountController( - SignInManager signInManager, - UserManager userManager, - IUserFactory userFactory, - IUserEvents userEvents, - IOptions identityOptions, - ISemanticLog log, - IIdentityServerInteractionService interactions) - { - this.log = log; - this.userEvents = userEvents; - this.userManager = userManager; - this.userFactory = userFactory; - this.interactions = interactions; - this.identityOptions = identityOptions.Value; - this.signInManager = signInManager; - } - - [HttpGet] - [Route("account/error/")] - public IActionResult LoginError() - { - throw new InvalidOperationException(); - } - - [HttpGet] - [Route("account/forbidden/")] - public IActionResult Forbidden() - { - throw new SecurityException("User is not allowed to login."); - } - - [HttpGet] - [Route("account/lockedout/")] - public IActionResult LockedOut() - { - return View(); - } - - [HttpGet] - [Route("account/accessdenied/")] - public IActionResult AccessDenied() - { - return View(); - } - - [HttpGet] - [Route("account/logout-completed/")] - public IActionResult LogoutCompleted() - { - return View(); - } - - [HttpGet] - [Route("account/consent/")] - public IActionResult Consent(string returnUrl = null) - { - return View(new ConsentVM { PrivacyUrl = identityOptions.PrivacyUrl, ReturnUrl = returnUrl }); - } - - [HttpPost] - [Route("account/consent/")] - public async Task Consent(ConsentModel model, string returnUrl = null) - { - if (!model.ConsentToCookies) - { - ModelState.AddModelError(nameof(model.ConsentToCookies), "You have to give consent."); - } - - if (!model.ConsentToPersonalInformation) - { - ModelState.AddModelError(nameof(model.ConsentToPersonalInformation), "You have to give consent."); - } - - if (!ModelState.IsValid) - { - var vm = new ConsentVM { PrivacyUrl = identityOptions.PrivacyUrl, ReturnUrl = returnUrl }; - - return View(vm); - } - - var user = await userManager.GetUserWithClaimsAsync(User); - - var update = new UserValues - { - Consent = true, - ConsentForEmails = model.ConsentToAutomatedEmails - }; - - await userManager.UpdateAsync(user.Id, update); - - userEvents.OnConsentGiven(user); - - return RedirectToReturnUrl(returnUrl); - } - - [HttpGet] - [Route("account/logout/")] - public async Task Logout(string logoutId) - { - var context = await interactions.GetLogoutContextAsync(logoutId); - - await signInManager.SignOutAsync(); - - return RedirectToLogoutUrl(context); - } - - [HttpGet] - [Route("account/logout-redirect/")] - public async Task LogoutRedirect() - { - await signInManager.SignOutAsync(); - - return RedirectToAction(nameof(LogoutCompleted)); - } - - [HttpGet] - [Route("account/signup/")] - public Task Signup(string returnUrl = null) - { - return LoginViewAsync(returnUrl, false, false); - } - - [HttpGet] - [Route("account/login/")] - [ClearCookies] - public Task Login(string returnUrl = null) - { - return LoginViewAsync(returnUrl, true, false); - } - - [HttpPost] - [Route("account/login/")] - public async Task Login(LoginModel model, string returnUrl = null) - { - if (!ModelState.IsValid) - { - return await LoginViewAsync(returnUrl, true, true); - } - - var result = await signInManager.PasswordSignInAsync(model.Email, model.Password, true, true); - - if (!result.Succeeded) - { - return await LoginViewAsync(returnUrl, true, true); - } - else - { - return RedirectToReturnUrl(returnUrl); - } - } - - private async Task LoginViewAsync(string returnUrl, bool isLogin, bool isFailed) - { - var allowPasswordAuth = identityOptions.AllowPasswordAuth; - - var externalProviders = await signInManager.GetExternalProvidersAsync(); - - if (externalProviders.Count == 1 && !allowPasswordAuth) - { - var provider = externalProviders[0].AuthenticationScheme; - - var properties = - signInManager.ConfigureExternalAuthenticationProperties(provider, - Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl })); - - return Challenge(properties, provider); - } - - var vm = new LoginVM - { - ExternalProviders = externalProviders, - IsLogin = isLogin, - IsFailed = isFailed, - HasPasswordAuth = allowPasswordAuth, - HasPasswordAndExternal = allowPasswordAuth && externalProviders.Any(), - ReturnUrl = returnUrl - }; - - return View(nameof(Login), vm); - } - - [HttpPost] - [Route("account/external/")] - public IActionResult External(string provider, string returnUrl = null) - { - var properties = - signInManager.ConfigureExternalAuthenticationProperties(provider, - Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl })); - - return Challenge(properties, provider); - } - - [HttpGet] - [Route("account/external-callback/")] - public async Task ExternalCallback(string returnUrl = null) - { - var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(); - - if (externalLogin == null) - { - return RedirectToAction(nameof(Login)); - } - - var result = await signInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); - - if (!result.Succeeded && result.IsLockedOut) - { - return View(nameof(LockedOut)); - } - - var isLoggedIn = result.Succeeded; - - UserWithClaims user; - - if (isLoggedIn) - { - user = await userManager.FindByLoginWithClaimsAsync(externalLogin.LoginProvider, externalLogin.ProviderKey); - } - else - { - var email = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value; - - user = await userManager.FindByEmailWithClaimsAsyncAsync(email); - - if (user != null) - { - isLoggedIn = - await AddLoginAsync(user, externalLogin) && - await AddClaimsAsync(user, externalLogin, email) && - await LoginAsync(externalLogin); - } - else - { - user = new UserWithClaims(userFactory.Create(email), new List()); - - var isFirst = userManager.Users.LongCount() == 0; - - isLoggedIn = - await AddUserAsync(user) && - await AddLoginAsync(user, externalLogin) && - await AddClaimsAsync(user, externalLogin, email, isFirst) && - await LockAsync(user, isFirst) && - await LoginAsync(externalLogin); - - userEvents.OnUserRegistered(user); - - if (await userManager.IsLockedOutAsync(user.Identity)) - { - return View(nameof(LockedOut)); - } - } - } - - if (!isLoggedIn) - { - return RedirectToAction(nameof(Login)); - } - else if (user != null && !user.HasConsent() && !identityOptions.NoConsent) - { - return RedirectToAction(nameof(Consent), new { returnUrl }); - } - else - { - return RedirectToReturnUrl(returnUrl); - } - } - - private Task AddLoginAsync(UserWithClaims user, UserLoginInfo externalLogin) - { - return MakeIdentityOperation(() => userManager.AddLoginAsync(user.Identity, externalLogin)); - } - - private Task AddUserAsync(UserWithClaims user) - { - return MakeIdentityOperation(() => userManager.CreateAsync(user.Identity)); - } - - private async Task LoginAsync(UserLoginInfo externalLogin) - { - var result = await signInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); - - return result.Succeeded; - } - - private Task LockAsync(UserWithClaims user, bool isFirst) - { - if (isFirst || !identityOptions.LockAutomatically) - { - return TaskHelper.True; - } - - return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user.Identity, DateTimeOffset.UtcNow.AddYears(100))); - } - - private Task AddClaimsAsync(UserWithClaims user, ExternalLoginInfo externalLogin, string email, bool isFirst = false) - { - var newClaims = new List(); - - void AddClaim(Claim claim) - { - newClaims.Add(claim); - - user.Claims.Add(claim); - } - - foreach (var squidexClaim in externalLogin.Principal.GetSquidexClaims()) - { - AddClaim(squidexClaim); - } - - if (!user.HasPictureUrl()) - { - AddClaim(new Claim(SquidexClaimTypes.PictureUrl, GravatarHelper.CreatePictureUrl(email))); - } - - if (!user.HasDisplayName()) - { - AddClaim(new Claim(SquidexClaimTypes.DisplayName, email)); - } - - if (isFirst) - { - AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.Admin)); - } - - return MakeIdentityOperation(() => userManager.SyncClaimsAsync(user.Identity, newClaims)); - } - - private IActionResult RedirectToLogoutUrl(LogoutRequest context) - { - if (!string.IsNullOrWhiteSpace(context.PostLogoutRedirectUri)) - { - return Redirect(context.PostLogoutRedirectUri); - } - else - { - return Redirect("~/../"); - } - } - - private IActionResult RedirectToReturnUrl(string returnUrl) - { - if (!string.IsNullOrWhiteSpace(returnUrl)) - { - return Redirect(returnUrl); - } - else - { - return Redirect("~/../"); - } - } - - private async Task MakeIdentityOperation(Func> action, [CallerMemberName] string operationName = null) - { - try - { - var result = await action(); - - if (!result.Succeeded) - { - var errorMessageBuilder = new StringBuilder(); - - foreach (var error in result.Errors) - { - errorMessageBuilder.Append(error.Code); - errorMessageBuilder.Append(": "); - errorMessageBuilder.AppendLine(error.Description); - } - - var errorMessage = errorMessageBuilder.ToString(); - - log.LogError((operationName, errorMessage), (ctx, w) => w - .WriteProperty("action", ctx.operationName) - .WriteProperty("status", "Failed") - .WriteProperty("message", ctx.errorMessage)); - } - - return result.Succeeded; - } - catch (Exception ex) - { - log.LogError(ex, operationName, (logOperationName, w) => w - .WriteProperty("action", logOperationName) - .WriteProperty("status", "Failed")); - - return false; - } - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs b/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs deleted file mode 100644 index a8764ff13..000000000 --- a/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Areas.IdentityServer.Controllers.Account -{ - public sealed class ConsentVM - { - public string ReturnUrl { get; set; } - - public string PrivacyUrl { get; set; } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs b/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs deleted file mode 100644 index 69a029bd8..000000000 --- a/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; - -namespace Squidex.Areas.IdentityServer.Controllers.Account -{ - public class LoginVM - { - public string ReturnUrl { get; set; } - - public bool IsLogin { get; set; } - - public bool IsFailed { get; set; } - - public bool HasPasswordAuth { get; set; } - - public bool HasPasswordAndExternal { get; set; } - - public IReadOnlyList ExternalProviders { get; set; } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs deleted file mode 100644 index 527dd12dd..000000000 --- a/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using IdentityServer4.Models; -using IdentityServer4.Services; -using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Squidex.Infrastructure; - -namespace Squidex.Areas.IdentityServer.Controllers.Error -{ - public sealed class ErrorController : IdentityServerController - { - private readonly IIdentityServerInteractionService interaction; - private readonly SignInManager signInManager; - - public ErrorController(IIdentityServerInteractionService interaction, SignInManager signInManager) - { - this.interaction = interaction; - this.signInManager = signInManager; - } - - [Route("error/")] - public async Task Error(string errorId = null) - { - await signInManager.SignOutAsync(); - - var vm = new ErrorViewModel(); - - if (!string.IsNullOrWhiteSpace(errorId)) - { - var message = await interaction.GetErrorContextAsync(errorId); - - if (message != null) - { - vm.Error = message; - } - } - - if (vm.Error == null) - { - var error = HttpContext.Features.Get()?.Error; - - if (error is DomainException exception) - { - vm.Error = new ErrorMessage { ErrorDescription = exception.Message }; - } - else if (error?.InnerException is DomainException exception2) - { - vm.Error = new ErrorMessage { ErrorDescription = exception2.Message }; - } - } - - return View("Error", vm); - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs b/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs deleted file mode 100644 index 652c61f79..000000000 --- a/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Microsoft.AspNetCore.Identity; - -namespace Squidex.Areas.IdentityServer.Controllers -{ - public static class Extensions - { - public static async Task GetExternalLoginInfoWithDisplayNameAsync(this SignInManager signInManager, string expectedXsrf = null) - { - var externalLogin = await signInManager.GetExternalLoginInfoAsync(expectedXsrf); - - var email = externalLogin.Principal.FindFirst(ClaimTypes.Email)?.Value; - - if (string.IsNullOrWhiteSpace(email)) - { - throw new InvalidOperationException("External provider does not provide email claim."); - } - - externalLogin.ProviderDisplayName = email; - - return externalLogin; - } - - public static async Task> GetExternalProvidersAsync(this SignInManager signInManager) - { - var externalSchemes = await signInManager.GetExternalAuthenticationSchemesAsync(); - - var externalProviders = - externalSchemes.Where(x => x.Name != OpenIdConnectDefaults.AuthenticationScheme) - .Select(x => new ExternalProvider(x.Name, x.DisplayName)).ToList(); - - return externalProviders; - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs deleted file mode 100644 index e28258681..000000000 --- a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs +++ /dev/null @@ -1,224 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Squidex.Config; -using Squidex.Domain.Users; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Reflection; -using Squidex.Shared.Identity; -using Squidex.Shared.Users; - -namespace Squidex.Areas.IdentityServer.Controllers.Profile -{ - [Authorize] - public sealed class ProfileController : IdentityServerController - { - private readonly SignInManager signInManager; - private readonly UserManager userManager; - private readonly IUserPictureStore userPictureStore; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - private readonly MyIdentityOptions identityOptions; - - public ProfileController( - SignInManager signInManager, - UserManager userManager, - IUserPictureStore userPictureStore, - IAssetThumbnailGenerator assetThumbnailGenerator, - IOptions identityOptions) - { - this.signInManager = signInManager; - this.identityOptions = identityOptions.Value; - this.userManager = userManager; - this.userPictureStore = userPictureStore; - this.assetThumbnailGenerator = assetThumbnailGenerator; - } - - [HttpGet] - [Route("/account/profile/")] - public async Task Profile(string successMessage = null) - { - var user = await userManager.GetUserWithClaimsAsync(User); - - return View(await GetProfileVM(user, successMessage: successMessage)); - } - - [HttpPost] - [Route("/account/profile/login-add/")] - public async Task AddLogin(string provider) - { - await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); - - var properties = - signInManager.ConfigureExternalAuthenticationProperties(provider, - Url.Action(nameof(AddLoginCallback)), userManager.GetUserId(User)); - - return Challenge(properties, provider); - } - - [HttpGet] - [Route("/account/profile/login-add-callback/")] - public Task AddLoginCallback() - { - return MakeChangeAsync(user => AddLoginAsync(user), - "Login added successfully."); - } - - [HttpPost] - [Route("/account/profile/update/")] - public Task UpdateProfile(ChangeProfileModel model) - { - return MakeChangeAsync(user => userManager.UpdateSafeAsync(user.Identity, model.ToValues()), - "Account updated successfully."); - } - - [HttpPost] - [Route("/account/profile/login-remove/")] - public Task RemoveLogin(RemoveLoginModel model) - { - return MakeChangeAsync(user => userManager.RemoveLoginAsync(user.Identity, model.LoginProvider, model.ProviderKey), - "Login provider removed successfully."); - } - - [HttpPost] - [Route("/account/profile/password-set/")] - public Task SetPassword(SetPasswordModel model) - { - return MakeChangeAsync(user => userManager.AddPasswordAsync(user.Identity, model.Password), - "Password set successfully."); - } - - [HttpPost] - [Route("/account/profile/password-change/")] - public Task ChangePassword(ChangePasswordModel model) - { - return MakeChangeAsync(user => userManager.ChangePasswordAsync(user.Identity, model.OldPassword, model.Password), - "Password changed successfully."); - } - - [HttpPost] - [Route("/account/profile/generate-client-secret/")] - public Task GenerateClientSecret() - { - return MakeChangeAsync(user => userManager.GenerateClientSecretAsync(user.Identity), - "Client secret generated successfully."); - } - - [HttpPost] - [Route("/account/profile/upload-picture/")] - public Task UploadPicture(List file) - { - return MakeChangeAsync(user => UpdatePictureAsync(file, user), - "Picture uploaded successfully."); - } - - private async Task AddLoginAsync(UserWithClaims user) - { - var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(userManager.GetUserId(User)); - - return await userManager.AddLoginAsync(user.Identity, externalLogin); - } - - private async Task UpdatePictureAsync(List file, UserWithClaims user) - { - if (file.Count != 1) - { - return IdentityResult.Failed(new IdentityError { Description = "Please upload a single file." }); - } - - using (var thumbnailStream = new MemoryStream()) - { - try - { - await assetThumbnailGenerator.CreateThumbnailAsync(file[0].OpenReadStream(), thumbnailStream, 128, 128, "Crop"); - - thumbnailStream.Position = 0; - } - catch - { - return IdentityResult.Failed(new IdentityError { Description = "Picture is not a valid image." }); - } - - await userPictureStore.UploadAsync(user.Id, thumbnailStream); - } - - return await userManager.UpdateSafeAsync(user.Identity, new UserValues { PictureUrl = SquidexClaimTypes.PictureUrlStore }); - } - - private async Task MakeChangeAsync(Func> action, string successMessage, ChangeProfileModel model = null) - { - var user = await userManager.GetUserWithClaimsAsync(User); - - if (!ModelState.IsValid) - { - return View(nameof(Profile), await GetProfileVM(user, model)); - } - - string errorMessage; - try - { - var result = await action(user); - - if (result.Succeeded) - { - await signInManager.SignInAsync(user.Identity, true); - - return RedirectToAction(nameof(Profile), new { successMessage }); - } - - errorMessage = string.Join(". ", result.Errors.Select(x => x.Description)); - } - catch - { - errorMessage = "An unexpected exception occurred."; - } - - return View(nameof(Profile), await GetProfileVM(user, model, errorMessage)); - } - - private async Task GetProfileVM(UserWithClaims user, ChangeProfileModel model = null, string errorMessage = null, string successMessage = null) - { - var taskForProviders = signInManager.GetExternalProvidersAsync(); - var taskForPassword = userManager.HasPasswordAsync(user.Identity); - var taskForLogins = userManager.GetLoginsAsync(user.Identity); - - await Task.WhenAll(taskForProviders, taskForPassword, taskForLogins); - - var result = new ProfileVM - { - Id = user.Id, - ClientSecret = user.ClientSecret(), - Email = user.Email, - ErrorMessage = errorMessage, - ExternalLogins = taskForLogins.Result, - ExternalProviders = taskForProviders.Result, - DisplayName = user.DisplayName(), - IsHidden = user.IsHidden(), - HasPassword = taskForPassword.Result, - HasPasswordAuth = identityOptions.AllowPasswordAuth, - SuccessMessage = successMessage - }; - - if (model != null) - { - SimpleMapper.Map(model, result); - } - - return result; - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs deleted file mode 100644 index e895f70e9..000000000 --- a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Microsoft.AspNetCore.Identity; - -namespace Squidex.Areas.IdentityServer.Controllers.Profile -{ - public sealed class ProfileVM - { - public string Id { get; set; } - - public string Email { get; set; } - - public string DisplayName { get; set; } - - public string ClientSecret { get; set; } - - public string ErrorMessage { get; set; } - - public string SuccessMessage { get; set; } - - public bool IsHidden { get; set; } - - public bool HasPassword { get; set; } - - public bool HasPasswordAuth { get; set; } - - public IList ExternalLogins { get; set; } - - public IList ExternalProviders { get; set; } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Startup.cs b/src/Squidex/Areas/IdentityServer/Startup.cs deleted file mode 100644 index d46ab509e..000000000 --- a/src/Squidex/Areas/IdentityServer/Startup.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Areas.IdentityServer.Config; -using Squidex.Web; - -namespace Squidex.Areas.IdentityServer -{ - public static class Startup - { - public static void ConfigureIdentityServer(this IApplicationBuilder app) - { - app.ApplicationServices.UseMyAdmin(); - - var environment = app.ApplicationServices.GetRequiredService(); - - app.Map(Constants.IdentityServerPrefix, identityApp => - { - if (!environment.IsDevelopment()) - { - identityApp.UseDeveloperExceptionPage(); - } - else - { - identityApp.UseExceptionHandler("/error"); - } - - identityApp.UseMyIdentityServer(); - - identityApp.UseMvc(); - }); - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Views/Extensions.cs b/src/Squidex/Areas/IdentityServer/Views/Extensions.cs deleted file mode 100644 index 814f9fb2a..000000000 --- a/src/Squidex/Areas/IdentityServer/Views/Extensions.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Squidex.Areas.IdentityServer.Views -{ - public static class Extensions - { - public static string RootContentUrl(this IUrlHelper urlHelper, string contentPath) - { - if (string.IsNullOrEmpty(contentPath)) - { - return null; - } - - if (contentPath[0] == '~') - { - var segment = new PathString(contentPath.Substring(1)); - - var applicationPath = urlHelper.ActionContext.HttpContext.Request.PathBase; - - if (applicationPath.HasValue) - { - var indexOfLastPart = applicationPath.Value.LastIndexOf('/'); - - if (indexOfLastPart >= 0) - { - applicationPath = applicationPath.Value.Substring(0, indexOfLastPart); - } - } - - return applicationPath.Add(segment).Value; - } - - return contentPath; - } - } -} diff --git a/src/Squidex/Areas/OrleansDashboard/Startup.cs b/src/Squidex/Areas/OrleansDashboard/Startup.cs deleted file mode 100644 index 943057450..000000000 --- a/src/Squidex/Areas/OrleansDashboard/Startup.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Builder; -using Orleans; -using Squidex.Areas.OrleansDashboard.Middlewares; -using Squidex.Web; - -namespace Squidex.Areas.OrleansDashboard -{ - public static class Startup - { - public static void ConfigureOrleansDashboard(this IApplicationBuilder app) - { - app.Map(Constants.OrleansPrefix, orleansApp => - { - orleansApp.UseAuthentication(); - orleansApp.UseMiddleware(); - orleansApp.UseOrleansDashboard(); - }); - } - } -} diff --git a/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs b/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs deleted file mode 100644 index dfaa742ce..000000000 --- a/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Squidex.Domain.Apps.Entities.Apps.Services; - -namespace Squidex.Areas.Portal.Middlewares -{ - public sealed class PortalRedirectMiddleware - { - private readonly IAppPlanBillingManager appPlansBillingManager; - - public PortalRedirectMiddleware(RequestDelegate next, IAppPlanBillingManager appPlansBillingManager) - { - this.appPlansBillingManager = appPlansBillingManager; - } - - public async Task Invoke(HttpContext context) - { - if (context.Request.Path == "/") - { - var userIdClaim = context.User.FindFirst(ClaimTypes.NameIdentifier); - - if (userIdClaim != null) - { - context.Response.RedirectToAbsoluteUrl(await appPlansBillingManager.GetPortalLinkAsync(userIdClaim.Value)); - } - } - } - } -} diff --git a/src/Squidex/Areas/Portal/Startup.cs b/src/Squidex/Areas/Portal/Startup.cs deleted file mode 100644 index 88cc7646b..000000000 --- a/src/Squidex/Areas/Portal/Startup.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Builder; -using Squidex.Areas.Portal.Middlewares; -using Squidex.Web; - -namespace Squidex.Areas.Portal -{ - public static class Startup - { - public static void ConfigurePortal(this IApplicationBuilder app) - { - app.Map(Constants.PortalPrefix, portalApp => - { - portalApp.UseAuthentication(); - portalApp.UseMiddleware(); - portalApp.UseMiddleware(); - }); - } - } -} diff --git a/src/Squidex/Config/Authentication/AuthenticationServices.cs b/src/Squidex/Config/Authentication/AuthenticationServices.cs deleted file mode 100644 index c086282f9..000000000 --- a/src/Squidex/Config/Authentication/AuthenticationServices.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Squidex.Config.Authentication -{ - public static class AuthenticationServices - { - public static void AddMyAuthentication(this IServiceCollection services, IConfiguration config) - { - var identityOptions = config.GetSection("identity").Get(); - - services.AddAuthentication() - .AddMyCookie() - .AddMyExternalGithubAuthentication(identityOptions) - .AddMyExternalGoogleAuthentication(identityOptions) - .AddMyExternalMicrosoftAuthentication(identityOptions) - .AddMyExternalOdic(identityOptions) - .AddMyIdentityServerAuthentication(identityOptions, config); - } - - public static AuthenticationBuilder AddMyCookie(this AuthenticationBuilder builder) - { - builder.Services.ConfigureApplicationCookie(options => - { - options.Cookie.Name = ".sq.auth"; - }); - - return builder.AddCookie(); - } - } -} diff --git a/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs b/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs deleted file mode 100644 index 2a618d892..000000000 --- a/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection; - -namespace Squidex.Config.Authentication -{ - public static class GithubAuthenticationServices - { - public static AuthenticationBuilder AddMyExternalGithubAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) - { - if (identityOptions.IsGithubAuthConfigured()) - { - authBuilder.AddGitHub(options => - { - options.ClientId = identityOptions.GithubClient; - options.ClientSecret = identityOptions.GithubSecret; - options.Events = new GithubHandler(); - }); - } - - return authBuilder; - } - } -} diff --git a/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs b/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs deleted file mode 100644 index 50a3d77a1..000000000 --- a/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection; - -namespace Squidex.Config.Authentication -{ - public static class GoogleAuthenticationServices - { - public static AuthenticationBuilder AddMyExternalGoogleAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) - { - if (identityOptions.IsGoogleAuthConfigured()) - { - authBuilder.AddGoogle(options => - { - options.ClientId = identityOptions.GoogleClient; - options.ClientSecret = identityOptions.GoogleSecret; - options.Events = new GoogleHandler(); - }); - } - - return authBuilder; - } - } -} diff --git a/src/Squidex/Config/Authentication/GoogleHandler.cs b/src/Squidex/Config/Authentication/GoogleHandler.cs deleted file mode 100644 index 9b3832f6b..000000000 --- a/src/Squidex/Config/Authentication/GoogleHandler.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.OAuth; -using Squidex.Infrastructure.Tasks; -using Squidex.Shared.Identity; - -namespace Squidex.Config.Authentication -{ - public sealed class GoogleHandler : OAuthEvents - { - public override Task RedirectToAuthorizationEndpoint(RedirectContext context) - { - context.Response.Redirect(context.RedirectUri + "&prompt=select_account"); - - return TaskHelper.Done; - } - - public override Task CreatingTicket(OAuthCreatingTicketContext context) - { - var nameClaim = context.Identity.FindFirst(ClaimTypes.Name)?.Value; - - if (!string.IsNullOrWhiteSpace(nameClaim)) - { - context.Identity.SetDisplayName(nameClaim); - } - - var pictureUrl = context.User?.Value("picture"); - - if (string.IsNullOrWhiteSpace(pictureUrl)) - { - pictureUrl = context.User?["image"]?.Value("url"); - - if (pictureUrl != null && pictureUrl.EndsWith("?sz=50", System.StringComparison.Ordinal)) - { - pictureUrl = pictureUrl.Substring(0, pictureUrl.Length - 6); - } - } - - if (!string.IsNullOrWhiteSpace(pictureUrl)) - { - context.Identity.SetPictureUrl(pictureUrl); - } - - return base.CreatingTicket(context); - } - } -} diff --git a/src/Squidex/Config/Authentication/IdentityServerServices.cs b/src/Squidex/Config/Authentication/IdentityServerServices.cs deleted file mode 100644 index f317247d3..000000000 --- a/src/Squidex/Config/Authentication/IdentityServerServices.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure; -using Squidex.Web; - -namespace Squidex.Config.Authentication -{ - public static class IdentityServerServices - { - public static AuthenticationBuilder AddMyIdentityServerAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions, IConfiguration config) - { - var apiScope = Constants.ApiScope; - - var urlsOptions = config.GetSection("urls").Get(); - - if (!string.IsNullOrWhiteSpace(urlsOptions.BaseUrl)) - { - string apiAuthorityUrl; - - if (!string.IsNullOrWhiteSpace(identityOptions.AuthorityUrl)) - { - apiAuthorityUrl = identityOptions.AuthorityUrl.BuildFullUrl(Constants.IdentityServerPrefix); - } - else - { - apiAuthorityUrl = urlsOptions.BuildUrl(Constants.IdentityServerPrefix); - } - - authBuilder.AddIdentityServerAuthentication(options => - { - options.Authority = apiAuthorityUrl; - options.ApiName = apiScope; - options.ApiSecret = null; - options.RequireHttpsMetadata = identityOptions.RequiresHttps; - }); - - authBuilder.AddOpenIdConnect(options => - { - options.Authority = apiAuthorityUrl; - options.ClientId = Constants.InternalClientId; - options.ClientSecret = Constants.InternalClientSecret; - options.CallbackPath = "/signin-internal"; - options.RequireHttpsMetadata = identityOptions.RequiresHttps; - options.SaveTokens = true; - options.Scope.Add(Constants.PermissionsScope); - options.Scope.Add(Constants.ProfileScope); - options.Scope.Add(Constants.RoleScope); - options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; - }); - } - - return authBuilder; - } - } -} diff --git a/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs b/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs deleted file mode 100644 index ea2091810..000000000 --- a/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection; - -namespace Squidex.Config.Authentication -{ - public static class MicrosoftAuthenticationServices - { - public static AuthenticationBuilder AddMyExternalMicrosoftAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) - { - if (identityOptions.IsMicrosoftAuthConfigured()) - { - authBuilder.AddMicrosoftAccount(options => - { - options.ClientId = identityOptions.MicrosoftClient; - options.ClientSecret = identityOptions.MicrosoftSecret; - options.Events = new MicrosoftHandler(); - }); - } - - return authBuilder; - } - } -} diff --git a/src/Squidex/Config/Authentication/MicrosoftHandler.cs b/src/Squidex/Config/Authentication/MicrosoftHandler.cs deleted file mode 100644 index 168995ad9..000000000 --- a/src/Squidex/Config/Authentication/MicrosoftHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication.OAuth; -using Squidex.Shared.Identity; - -namespace Squidex.Config.Authentication -{ - public sealed class MicrosoftHandler : OAuthEvents - { - public override Task CreatingTicket(OAuthCreatingTicketContext context) - { - var displayName = context.User.Value("displayName"); - - if (!string.IsNullOrEmpty(displayName)) - { - context.Identity.SetDisplayName(displayName); - } - - var id = context.User.Value("id"); - - if (!string.IsNullOrEmpty(id)) - { - var pictureUrl = $"https://apis.live.net/v5.0/{id}/picture"; - - context.Identity.SetPictureUrl(pictureUrl); - } - - return base.CreatingTicket(context); - } - } -} diff --git a/src/Squidex/Config/Authentication/OidcServices.cs b/src/Squidex/Config/Authentication/OidcServices.cs deleted file mode 100644 index 4a490ca97..000000000 --- a/src/Squidex/Config/Authentication/OidcServices.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Microsoft.Extensions.DependencyInjection; - -namespace Squidex.Config.Authentication -{ - public static class OidcServices - { - public static AuthenticationBuilder AddMyExternalOdic(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) - { - if (identityOptions.IsOidcConfigured()) - { - var displayName = !string.IsNullOrWhiteSpace(identityOptions.OidcName) ? identityOptions.OidcName : OpenIdConnectDefaults.DisplayName; - - authBuilder.AddOpenIdConnect("ExternalOidc", displayName, options => - { - options.Authority = identityOptions.OidcAuthority; - options.ClientId = identityOptions.OidcClient; - options.ClientSecret = identityOptions.OidcSecret; - options.RequireHttpsMetadata = false; - options.Events = new OidcHandler(identityOptions); - - if (identityOptions.OidcScopes != null) - { - foreach (var scope in identityOptions.OidcScopes) - { - options.Scope.Add(scope); - } - } - }); - } - - return authBuilder; - } - } -} diff --git a/src/Squidex/Config/Domain/AssetServices.cs b/src/Squidex/Config/Domain/AssetServices.cs deleted file mode 100644 index 90b5ac434..000000000 --- a/src/Squidex/Config/Domain/AssetServices.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using FluentFTP; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; -using MongoDB.Driver.GridFS; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Assets.ImageSharp; -using Squidex.Infrastructure.Log; - -namespace Squidex.Config.Domain -{ - public static class AssetServices - { - public static void AddMyAssetServices(this IServiceCollection services, IConfiguration config) - { - config.ConfigureByOption("assetStore:type", new Alternatives - { - ["Default"] = () => - { - services.AddSingletonAs() - .AsOptional(); - }, - ["Folder"] = () => - { - var path = config.GetRequiredValue("assetStore:folder:path"); - - services.AddSingletonAs(c => new FolderAssetStore(path, c.GetRequiredService())) - .As(); - }, - ["GoogleCloud"] = () => - { - var bucketName = config.GetRequiredValue("assetStore:googleCloud:bucket"); - - services.AddSingletonAs(c => new GoogleCloudAssetStore(bucketName)) - .As(); - }, - ["AzureBlob"] = () => - { - var connectionString = config.GetRequiredValue("assetStore:azureBlob:connectionString"); - var containerName = config.GetRequiredValue("assetStore:azureBlob:containerName"); - - services.AddSingletonAs(c => new AzureBlobAssetStore(connectionString, containerName)) - .As(); - }, - ["MongoDb"] = () => - { - var mongoConfiguration = config.GetRequiredValue("assetStore:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("assetStore:mongoDb:database"); - var mongoGridFsBucketName = config.GetRequiredValue("assetStore:mongoDb:bucket"); - - services.AddSingletonAs(c => - { - var mongoClient = Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s)); - var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); - - var gridFsbucket = new GridFSBucket(mongoDatabase, new GridFSBucketOptions - { - BucketName = mongoGridFsBucketName - }); - - return new MongoGridFsAssetStore(gridFsbucket); - }) - .As(); - }, - ["Ftp"] = () => - { - var serverHost = config.GetRequiredValue("assetStore:ftp:serverHost"); - var serverPort = config.GetOptionalValue("assetStore:ftp:serverPort", 21); - - var username = config.GetRequiredValue("assetStore:ftp:username"); - var password = config.GetRequiredValue("assetStore:ftp:password"); - - var path = config.GetOptionalValue("assetStore:ftp:path", "/"); - - services.AddSingletonAs(c => - { - var factory = new Func(() => new FtpClient(serverHost, serverPort, username, password)); - - return new FTPAssetStore(factory, path, c.GetRequiredService()); - }) - .As(); - } - }); - - services.AddSingletonAs() - .As(); - } - } -} diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs deleted file mode 100644 index cc8cb02f8..000000000 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ /dev/null @@ -1,371 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using GraphQL; -using GraphQL.DataLoader; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Migrate_01; -using Migrate_01.Migrations; -using Orleans; -using Squidex.Areas.Api.Controllers.UI; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Indexes; -using Squidex.Domain.Apps.Entities.Apps.Invitation; -using Squidex.Domain.Apps.Entities.Apps.Templates; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Domain.Apps.Entities.Assets.Queries; -using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Entities.Comments; -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.GraphQL; -using Squidex.Domain.Apps.Entities.Contents.Queries; -using Squidex.Domain.Apps.Entities.Contents.Text; -using Squidex.Domain.Apps.Entities.History; -using Squidex.Domain.Apps.Entities.History.Notifications; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Rules.Indexes; -using Squidex.Domain.Apps.Entities.Rules.Queries; -using Squidex.Domain.Apps.Entities.Rules.UsageTracking; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.Schemas.Indexes; -using Squidex.Domain.Apps.Entities.Tags; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Email; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Migrations; -using Squidex.Infrastructure.Orleans; -using Squidex.Web; -using Squidex.Web.CommandMiddlewares; -using Squidex.Web.Services; - -namespace Squidex.Config.Domain -{ - public static class EntitiesServices - { - public static void AddMyEntitiesServices(this IServiceCollection services, IConfiguration config) - { - var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true); - - services.AddSingletonAs(c => new UrlGenerator( - c.GetRequiredService>(), - c.GetRequiredService(), - exposeSourceUrl)) - .As().As().As().As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs(x => new FuncDependencyResolver(t => x.GetRequiredService(t))) - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs(c => new Lazy(() => c.GetRequiredService())) - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsOptional(); - - services.AddSingletonAs() - .AsOptional(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs() - .As>(); - - services.AddSingletonAs() - .As>(); - - services.AddSingletonAs() - .AsOptional(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs>() - .AsSelf(); - - services.AddSingletonAs>() - .AsSelf(); - - services.AddCommandPipeline(); - services.AddBackupHandlers(); - - services.AddSingleton>(DomainObjectGrainFormatter.Format); - - services.AddSingleton(c => - { - var uiOptions = c.GetRequiredService>(); - - var result = new InitialPatterns(); - - foreach (var (key, value) in uiOptions.Value.RegexSuggestions) - { - if (!string.IsNullOrWhiteSpace(key) && - !string.IsNullOrWhiteSpace(value)) - { - result[Guid.NewGuid()] = new AppPattern(key, value); - } - } - - return result; - }); - - var emailOptions = config.GetSection("email:smtp").Get(); - - if (emailOptions.IsConfigured()) - { - services.AddSingleton(Options.Create(emailOptions)); - - services.Configure( - config.GetSection("email:notifications")); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsOptional(); - } - else - { - services.AddSingletonAs() - .AsOptional(); - } - - services.AddSingletonAs() - .As(); - } - - private static void AddCommandPipeline(this IServiceCollection services) - { - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs>() - .As(); - - services.AddSingletonAs>() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingleton(typeof(IEventEnricher<>), typeof(SquidexEventEnricher<>)); - } - - private static void AddBackupHandlers(this IServiceCollection services) - { - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - } - - public static void AddMyMigrationServices(this IServiceCollection services) - { - services.AddSingletonAs() - .AsSelf(); - - services.AddTransientAs() - .AsSelf(); - - services.AddTransientAs() - .AsSelf(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - } - } -} diff --git a/src/Squidex/Config/Domain/EventPublishersServices.cs b/src/Squidex/Config/Domain/EventPublishersServices.cs deleted file mode 100644 index 623ab9ca5..000000000 --- a/src/Squidex/Config/Domain/EventPublishersServices.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; - -namespace Squidex.Config.Domain -{ - public static class EventPublishersServices - { - public static void AddMyEventPublishersServices(this IServiceCollection services, IConfiguration config) - { - var eventPublishers = config.GetSection("eventPublishers"); - - foreach (var child in eventPublishers.GetChildren()) - { - var eventPublisherType = child.GetValue("type"); - - if (string.IsNullOrWhiteSpace(eventPublisherType)) - { - throw new ConfigurationException($"Configure EventPublisher type with 'eventPublishers:{child.Key}:type'."); - } - - var eventsFilter = child.GetValue("eventsFilter"); - - var enabled = child.GetValue("enabled"); - - if (string.Equals(eventPublisherType, "RabbitMq", StringComparison.OrdinalIgnoreCase)) - { - var publisherConfig = child.GetValue("configuration"); - - if (string.IsNullOrWhiteSpace(publisherConfig)) - { - throw new ConfigurationException($"Configure EventPublisher RabbitMq configuration with 'eventPublishers:{child.Key}:configuration'."); - } - - var exchange = child.GetValue("exchange"); - - if (string.IsNullOrWhiteSpace(exchange)) - { - throw new ConfigurationException($"Configure EventPublisher RabbitMq exchange with 'eventPublishers:{child.Key}:configuration'."); - } - - var name = $"EventPublishers_{child.Key}"; - - if (enabled) - { - services.AddSingletonAs(c => new RabbitMqEventConsumer(c.GetRequiredService(), name, publisherConfig, exchange, eventsFilter)) - .As(); - } - } - else - { - throw new ConfigurationException($"Unsupported value '{child.Key}' for 'eventPublishers:{child.Key}:type', supported: RabbitMq."); - } - } - } - } -} diff --git a/src/Squidex/Config/Domain/EventStoreServices.cs b/src/Squidex/Config/Domain/EventStoreServices.cs deleted file mode 100644 index 3f9158605..000000000 --- a/src/Squidex/Config/Domain/EventStoreServices.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using EventStore.ClientAPI; -using Microsoft.Azure.Documents.Client; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; -using Newtonsoft.Json; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Diagnostics; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.EventSourcing.Grains; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.States; - -namespace Squidex.Config.Domain -{ - public static class EventStoreServices - { - public static void AddMyEventStoreServices(this IServiceCollection services, IConfiguration config) - { - config.ConfigureByOption("eventStore:type", new Alternatives - { - ["MongoDb"] = () => - { - var mongoConfiguration = config.GetRequiredValue("eventStore:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("eventStore:mongoDb:database"); - - services.AddSingletonAs(c => - { - var mongoClient = Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s)); - var mongDatabase = mongoClient.GetDatabase(mongoDatabaseName); - - return new MongoEventStore(mongDatabase, c.GetRequiredService()); - }) - .As(); - }, - ["CosmosDb"] = () => - { - var cosmosDbConfiguration = config.GetRequiredValue("eventStore:cosmosDB:configuration"); - var cosmosDbMasterKey = config.GetRequiredValue("eventStore:cosmosDB:masterKey"); - var cosmosDbDatabase = config.GetRequiredValue("eventStore:cosmosDB:database"); - - services.AddSingletonAs(c => new DocumentClient(new Uri(cosmosDbConfiguration), cosmosDbMasterKey, c.GetRequiredService())) - .AsSelf(); - - services.AddSingletonAs(c => new CosmosDbEventStore( - c.GetRequiredService(), - cosmosDbMasterKey, - cosmosDbDatabase, - c.GetRequiredService())) - .As(); - - services.AddHealthChecks() - .AddCheck("CosmosDB", tags: new[] { "node" }); - }, - ["GetEventStore"] = () => - { - var eventStoreConfiguration = config.GetRequiredValue("eventStore:getEventStore:configuration"); - var eventStoreProjectionHost = config.GetRequiredValue("eventStore:getEventStore:projectionHost"); - var eventStorePrefix = config.GetValue("eventStore:getEventStore:prefix"); - - services.AddSingletonAs(_ => EventStoreConnection.Create(eventStoreConfiguration)) - .As(); - - services.AddSingletonAs(c => new GetEventStore( - c.GetRequiredService(), - c.GetRequiredService(), - eventStorePrefix, - eventStoreProjectionHost)) - .As(); - - services.AddHealthChecks() - .AddCheck("EventStore", tags: new[] { "node" }); - } - }); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs(c => - { - var allEventConsumers = c.GetServices(); - - return new EventConsumerFactory(n => allEventConsumers.FirstOrDefault(x => x.Name == n)); - }); - } - } -} diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs deleted file mode 100644 index e47181641..000000000 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.DataProtection.Repositories; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using NodaTime; -using Squidex.Areas.Api.Controllers.News.Service; -using Squidex.Domain.Apps.Entities.Apps.Diagnostics; -using Squidex.Domain.Apps.Entities.Rules.UsageTracking; -using Squidex.Domain.Users; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Caching; -using Squidex.Infrastructure.Diagnostics; -using Squidex.Infrastructure.EventSourcing.Grains; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Translations; -using Squidex.Infrastructure.UsageTracking; -using Squidex.Shared.Users; - -#pragma warning disable RECS0092 // Convert field to readonly - -namespace Squidex.Config.Domain -{ - public static class InfrastructureServices - { - public static void AddMyInfrastructureServices(this IServiceCollection services, IConfiguration config) - { - services.AddHealthChecks() - .AddCheck("GC", tags: new[] { "node" }) - .AddCheck("Orleans", tags: new[] { "cluster" }) - .AddCheck("Orleans App", tags: new[] { "cluster" }); - - services.AddSingletonAs(c => new CachingUsageTracker( - c.GetRequiredService(), - c.GetRequiredService())) - .As(); - - services.AddSingletonAs(_ => SystemClock.Instance) - .As(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs>() - .AsSelf(); - - services.AddSingletonAs>() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsOptional(); - - services.AddSingletonAs() - .AsOptional(); - - services.AddSingletonAs() - .As(); - } - } -} diff --git a/src/Squidex/Config/Domain/LoggingExtensions.cs b/src/Squidex/Config/Domain/LoggingExtensions.cs deleted file mode 100644 index c11379044..000000000 --- a/src/Squidex/Config/Domain/LoggingExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure.Log; - -namespace Squidex.Config.Domain -{ - public static class LoggingExtensions - { - public static void LogConfiguration(this IServiceProvider services) - { - var log = services.GetRequiredService(); - - log.LogInformation(w => w - .WriteProperty("message", "Application started") - .WriteObject("environment", c => - { - var config = services.GetRequiredService(); - - var logged = new HashSet(StringComparer.OrdinalIgnoreCase); - - var orderedConfigs = config.AsEnumerable().Where(kvp => kvp.Value != null).OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase); - - foreach (var (key, val) in orderedConfigs) - { - if (logged.Add(key)) - { - c.WriteProperty(key.ToLowerInvariant(), val); - } - } - })); - } - } -} diff --git a/src/Squidex/Config/Domain/LoggingServices.cs b/src/Squidex/Config/Domain/LoggingServices.cs deleted file mode 100644 index f78858a8c..000000000 --- a/src/Squidex/Config/Domain/LoggingServices.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Infrastructure.Log; -using Squidex.Web.Pipeline; - -namespace Squidex.Config.Domain -{ - public static class LoggingServices - { - public static void AddMyLoggingServices(this IServiceCollection services, IConfiguration config) - { - if (config.GetValue("logging:human")) - { - services.AddSingletonAs(_ => JsonLogWriterFactory.Readable()) - .As(); - } - else - { - services.AddSingletonAs(_ => JsonLogWriterFactory.Default()) - .As(); - } - - var loggingFile = config.GetValue("logging:file"); - - if (!string.IsNullOrWhiteSpace(loggingFile)) - { - services.AddSingletonAs(_ => new FileChannel(loggingFile)) - .As(); - } - - var useColors = config.GetValue("logging:colors"); - - services.AddSingletonAs(_ => new ConsoleLogChannel(useColors)) - .As(); - - services.AddSingletonAs(_ => new ApplicationInfoLogAppender(typeof(Program).Assembly, Guid.NewGuid())) - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsOptional(); - } - } -} diff --git a/src/Squidex/Config/Domain/RuleServices.cs b/src/Squidex/Config/Domain/RuleServices.cs deleted file mode 100644 index 0e023d931..000000000 --- a/src/Squidex/Config/Domain/RuleServices.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.DependencyInjection; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Rules.UsageTracking; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Config.Domain -{ - public static class RuleServices - { - public static void AddMyRuleServices(this IServiceCollection services) - { - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs() - .As().AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - } - } -} diff --git a/src/Squidex/Config/Domain/SerializationServices.cs b/src/Squidex/Config/Domain/SerializationServices.cs deleted file mode 100644 index 20c03a60c..000000000 --- a/src/Squidex/Config/Domain/SerializationServices.cs +++ /dev/null @@ -1,124 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.DependencyInjection; -using Migrate_01; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Apps.Json; -using Squidex.Domain.Apps.Core.Contents.Json; -using Squidex.Domain.Apps.Core.Rules.Json; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Schemas.Json; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Newtonsoft; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Config.Domain -{ - public static class SerializationServices - { - private static JsonSerializerSettings ConfigureJson(JsonSerializerSettings settings, TypeNameHandling typeNameHandling) - { - settings.Converters.Add(new StringEnumConverter()); - - settings.ContractResolver = new ConverterContractResolver( - new AppClientsConverter(), - new AppContributorsConverter(), - new AppPatternsConverter(), - new ClaimsPrincipalConverter(), - new ContentFieldDataConverter(), - new EnvelopeHeadersConverter(), - new FilterConverter(), - new InstantConverter(), - new JsonValueConverter(), - new LanguageConverter(), - new LanguagesConfigConverter(), - new NamedGuidIdConverter(), - new NamedLongIdConverter(), - new NamedStringIdConverter(), - new PropertyPathConverter(), - new RefTokenConverter(), - new RolesConverter(), - new RuleConverter(), - new SchemaConverter(), - new StatusConverter(), - new StringEnumConverter(), - new WorkflowConverter(), - new WorkflowTransitionConverter()); - - settings.NullValueHandling = NullValueHandling.Ignore; - - settings.DateFormatHandling = DateFormatHandling.IsoDateFormat; - settings.DateParseHandling = DateParseHandling.None; - - settings.TypeNameHandling = typeNameHandling; - - return settings; - } - - public static IServiceCollection AddMySerializers(this IServiceCollection services) - { - services.AddSingletonAs>() - .As(); - - services.AddSingletonAs>() - .As(); - - services.AddSingletonAs>() - .As(); - - services.AddSingletonAs>() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs(c => JsonSerializer.Create(c.GetRequiredService())) - .AsSelf(); - - services.AddSingletonAs(c => - { - var serializerSettings = ConfigureJson(new JsonSerializerSettings(), TypeNameHandling.Auto); - - var typeNameRegistry = c.GetService(); - - if (typeNameRegistry != null) - { - serializerSettings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); - } - - return serializerSettings; - }).As(); - - return services; - } - - public static IMvcBuilder AddMySerializers(this IMvcBuilder mvc) - { - mvc.AddJsonOptions(options => - { - ConfigureJson(options.SerializerSettings, TypeNameHandling.None); - }); - - return mvc; - } - } -} diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs deleted file mode 100644 index 7f1149f24..000000000 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ /dev/null @@ -1,126 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using IdentityServer4.Stores; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Migrate_01.Migrations.MongoDb; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.Assets.State; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Contents.State; -using Squidex.Domain.Apps.Entities.Contents.Text; -using Squidex.Domain.Apps.Entities.History.Repositories; -using Squidex.Domain.Apps.Entities.MongoDb.Assets; -using Squidex.Domain.Apps.Entities.MongoDb.Contents; -using Squidex.Domain.Apps.Entities.MongoDb.History; -using Squidex.Domain.Apps.Entities.MongoDb.Rules; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Domain.Users; -using Squidex.Domain.Users.MongoDb; -using Squidex.Domain.Users.MongoDb.Infrastructure; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Diagnostics; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Migrations; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.UsageTracking; - -namespace Squidex.Config.Domain -{ - public static class StoreServices - { - public static void AddMyStoreServices(this IServiceCollection services, IConfiguration config) - { - config.ConfigureByOption("store:type", new Alternatives - { - ["MongoDB"] = () => - { - var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); - var mongoContentDatabaseName = config.GetOptionalValue("store:mongoDb:contentDatabase", mongoDatabaseName); - - services.AddSingleton(typeof(ISnapshotStore<,>), typeof(MongoSnapshotStore<,>)); - - services.AddSingletonAs(_ => Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s))) - .As(); - - services.AddSingletonAs(c => c.GetRequiredService().GetDatabase(mongoDatabaseName)) - .As(); - - services.AddTransientAs(c => new DeleteContentCollections(c.GetRequiredService().GetDatabase(mongoContentDatabaseName))) - .As(); - - services.AddTransientAs(c => new RestructureContentCollection(c.GetRequiredService().GetDatabase(mongoContentDatabaseName))) - .As(); - - services.AddSingletonAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddHealthChecks() - .AddCheck("MongoDB", tags: new[] { "node" }); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As>(); - - services.AddSingletonAs() - .As>() - .As(); - - services.AddSingletonAs() - .As() - .As>(); - - services.AddSingletonAs(c => new MongoContentRepository( - c.GetRequiredService().GetDatabase(mongoContentDatabaseName), - c.GetRequiredService(), - c.GetRequiredService(), - c.GetRequiredService(), - c.GetRequiredService())) - .As() - .As>() - .As(); - - var registration = services.FirstOrDefault(x => x.ServiceType == typeof(IPersistedGrantStore)); - - if (registration == null || registration.ImplementationType == typeof(InMemoryPersistedGrantStore)) - { - services.AddSingletonAs() - .As(); - } - } - }); - - services.AddSingleton(typeof(IStore<>), typeof(Store<>)); - } - } -} diff --git a/src/Squidex/Config/Domain/SubscriptionServices.cs b/src/Squidex/Config/Domain/SubscriptionServices.cs deleted file mode 100644 index 82e370e7b..000000000 --- a/src/Squidex/Config/Domain/SubscriptionServices.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; -using Squidex.Domain.Users; -using Squidex.Infrastructure; -using Squidex.Web; - -namespace Squidex.Config.Domain -{ - public static class SubscriptionServices - { - public static void AddMySubscriptionServices(this IServiceCollection services, IConfiguration config) - { - services.AddSingletonAs(c => c.GetRequiredService>()?.Value?.Plans.OrEmpty()); - - services.AddSingletonAs() - .AsOptional(); - - services.AddSingletonAs() - .AsOptional(); - - services.AddSingletonAs() - .AsOptional(); - } - } -} diff --git a/src/Squidex/Config/Logging.cs b/src/Squidex/Config/Logging.cs deleted file mode 100644 index df98f9edc..000000000 --- a/src/Squidex/Config/Logging.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -#define LOG_ALL_IDENTITY_SERVER_NONE - -using System; -using Microsoft.Extensions.Logging; - -namespace Squidex.Config -{ - public static class Logging - { - public static void AddFilters(this ILoggingBuilder builder) - { - builder.AddFilter((category, level) => - { - if (level < LogLevel.Information) - { - return false; - } - - if (category.StartsWith("Orleans.", StringComparison.OrdinalIgnoreCase)) - { - var subCategory = category.AsSpan().Slice(8); - - if (subCategory.StartsWith("Runtime.")) - { - var subCategory2 = subCategory.Slice(8); - - if (subCategory.StartsWith("NoOpHostEnvironmentStatistics", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Error; - } - - if (subCategory.StartsWith("SafeTimer", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Error; - } - } - - return level >= LogLevel.Warning; - } - - if (category.StartsWith("Runtime.", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Warning; - } - - if (category.StartsWith("Microsoft.AspNetCore.", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Warning; - } -#if LOG_ALL_IDENTITY_SERVER - if (category.StartsWith("IdentityServer4.", StringComparison.OrdinalIgnoreCase)) - { - return true; - } -#endif - return true; - }); - } - } -} diff --git a/src/Squidex/Config/Orleans/OrleansServices.cs b/src/Squidex/Config/Orleans/OrleansServices.cs deleted file mode 100644 index a00d44982..000000000 --- a/src/Squidex/Config/Orleans/OrleansServices.cs +++ /dev/null @@ -1,132 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Net; -using System.Net.Sockets; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Orleans; -using Orleans.Configuration; -using Orleans.Hosting; -using Orleans.Providers.MongoDB.Configuration; -using OrleansDashboard; -using Squidex.Domain.Apps.Entities; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Squidex.Web; -using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; - -namespace Squidex.Config.Orleans -{ - public static class OrleansServices - { - public static IServiceCollection AddOrleans(this IServiceCollection services, IConfiguration config, IWebHostEnvironment environment) - { - services.AddOrleans(config, environment, builder => - { - builder.ConfigureServices(siloServices => - { - siloServices.AddSingleton(); - siloServices.AddScoped(); - - siloServices.AddScoped(typeof(IGrainState<>), typeof(GrainState<>)); - }); - - builder.ConfigureApplicationParts(parts => - { - parts.AddApplicationPart(SquidexEntities.Assembly); - parts.AddApplicationPart(SquidexInfrastructure.Assembly); - }); - - builder.Configure(options => - { - options.Configure(); - }); - - builder.Configure(options => - { - options.FastKillOnProcessExit = false; - }); - - builder.Configure(options => - { - options.HideTrace = true; - }); - - builder.UseDashboard(options => - { - options.HostSelf = false; - }); - - builder.AddIncomingGrainCallFilter(); - builder.AddIncomingGrainCallFilter(); - builder.AddIncomingGrainCallFilter(); - builder.AddIncomingGrainCallFilter(); - - var orleansPortSilo = config.GetOptionalValue("orleans:siloPort", 11111); - var orleansPortGateway = config.GetOptionalValue("orleans:gatewayPort", 40000); - - var address = Helper.ResolveIPAddressAsync(Dns.GetHostName(), AddressFamily.InterNetwork).Result; - - builder.ConfigureEndpoints( - address, - orleansPortSilo, - orleansPortGateway, - true); - - config.ConfigureByOption("orleans:clustering", new Alternatives - { - ["MongoDB"] = () => - { - builder.UseMongoDBClustering(options => - { - options.Configure(config); - }); - }, - ["Development"] = () => - { - builder.UseDevelopmentClustering(new IPEndPoint(address, orleansPortSilo)); - - builder.Configure(options => - { - options.ExpectedClusterSize = 1; - }); - } - }); - - config.ConfigureByOption("store:type", new Alternatives - { - ["MongoDB"] = () => - { - builder.UseMongoDBReminders(options => - { - options.Configure(config); - }); - } - }); - }); - - return services; - } - - private static void Configure(this MongoDBOptions options, IConfiguration config) - { - var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); - - options.ConnectionString = mongoConfiguration; - options.CollectionPrefix = "Orleans_"; - options.DatabaseName = mongoDatabaseName; - } - - private static void Configure(this ClusterOptions options) - { - options.ClusterId = Constants.OrleansClusterId; - options.ServiceId = Constants.OrleansClusterId; - } - } -} diff --git a/src/Squidex/Config/Startup/BackgroundHost.cs b/src/Squidex/Config/Startup/BackgroundHost.cs deleted file mode 100644 index 41bef8f72..000000000 --- a/src/Squidex/Config/Startup/BackgroundHost.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. - -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Config.Startup -{ - public sealed class BackgroundHost : SafeHostedService - { - private readonly IEnumerable targets; - - public BackgroundHost(IEnumerable targets, IApplicationLifetime lifetime, ISemanticLog log) - : base(lifetime, log) - { - this.targets = targets; - } - - protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) - { - foreach (var target in targets.Distinct()) - { - await target.StartAsync(ct); - - log.LogInformation(w => w.WriteProperty("backgroundSystem", target.ToString())); - } - } - } -} diff --git a/src/Squidex/Config/Startup/InitializerHost.cs b/src/Squidex/Config/Startup/InitializerHost.cs deleted file mode 100644 index 9d8e2790a..000000000 --- a/src/Squidex/Config/Startup/InitializerHost.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Config.Startup -{ - public sealed class InitializerHost : SafeHostedService - { - private readonly IEnumerable targets; - - public InitializerHost(IEnumerable targets, IApplicationLifetime lifetime, ISemanticLog log) - : base(lifetime, log) - { - this.targets = targets; - } - - protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) - { - foreach (var target in targets.Distinct()) - { - await target.InitializeAsync(ct); - - log.LogInformation(w => w.WriteProperty("initializedSystem", target.GetType().Name)); - } - } - } -} diff --git a/src/Squidex/Config/Startup/MigrationRebuilderHost.cs b/src/Squidex/Config/Startup/MigrationRebuilderHost.cs deleted file mode 100644 index 8a1c84ea0..000000000 --- a/src/Squidex/Config/Startup/MigrationRebuilderHost.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Migrate_01; -using Squidex.Infrastructure.Log; - -namespace Squidex.Config.Startup -{ - public sealed class MigrationRebuilderHost : SafeHostedService - { - private readonly RebuildRunner rebuildRunner; - - public MigrationRebuilderHost(IApplicationLifetime lifetime, ISemanticLog log, RebuildRunner rebuildRunner) - : base(lifetime, log) - { - this.rebuildRunner = rebuildRunner; - } - - protected override Task StartAsync(ISemanticLog log, CancellationToken ct) - { - return rebuildRunner.RunAsync(ct); - } - } -} diff --git a/src/Squidex/Config/Startup/MigratorHost.cs b/src/Squidex/Config/Startup/MigratorHost.cs deleted file mode 100644 index 53ff2305e..000000000 --- a/src/Squidex/Config/Startup/MigratorHost.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Migrations; - -namespace Squidex.Config.Startup -{ - public sealed class MigratorHost : SafeHostedService - { - private readonly Migrator migrator; - - public MigratorHost(Migrator migrator, IApplicationLifetime lifetime, ISemanticLog log) - : base(lifetime, log) - { - this.migrator = migrator; - } - - protected override Task StartAsync(ISemanticLog log, CancellationToken ct) - { - return migrator.MigrateAsync(ct); - } - } -} diff --git a/src/Squidex/Config/Startup/SafeHostedService.cs b/src/Squidex/Config/Startup/SafeHostedService.cs deleted file mode 100644 index 90f39c691..000000000 --- a/src/Squidex/Config/Startup/SafeHostedService.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Config.Startup -{ - public abstract class SafeHostedService : IHostedService - { - private readonly IApplicationLifetime lifetime; - private readonly ISemanticLog log; - private bool isStarted; - - protected SafeHostedService(IApplicationLifetime lifetime, ISemanticLog log) - { - this.lifetime = lifetime; - - this.log = log; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - try - { - await StartAsync(log, cancellationToken); - - isStarted = true; - } - catch - { - lifetime.StopApplication(); - throw; - } - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - if (isStarted) - { - await StopAsync(log, cancellationToken); - } - } - - protected abstract Task StartAsync(ISemanticLog log, CancellationToken ct); - - protected virtual Task StopAsync(ISemanticLog log, CancellationToken ct) - { - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex/Config/Web/WebExtensions.cs b/src/Squidex/Config/Web/WebExtensions.cs deleted file mode 100644 index badab15c3..000000000 --- a/src/Squidex/Config/Web/WebExtensions.cs +++ /dev/null @@ -1,121 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Net.Http.Headers; -using Squidex.Infrastructure.Json; -using Squidex.Pipeline.Robots; -using Squidex.Web.Pipeline; - -namespace Squidex.Config.Web -{ - public static class WebExtensions - { - public static IApplicationBuilder UseMyLocalCache(this IApplicationBuilder app) - { - app.UseMiddleware(); - - return app; - } - - public static IApplicationBuilder UseMyTracking(this IApplicationBuilder app) - { - app.UseMiddleware(); - - return app; - } - - public static IApplicationBuilder UseMyHealthCheck(this IApplicationBuilder app) - { - var serializer = app.ApplicationServices.GetRequiredService(); - - var writer = new Func((httpContext, report) => - { - var response = new - { - Entries = report.Entries.ToDictionary(x => x.Key, x => - { - var value = x.Value; - - return new - { - Data = value.Data.Count > 0 ? new Dictionary(value.Data) : null, - value.Description, - value.Duration, - value.Status - }; - }), - report.Status, - report.TotalDuration - }; - - var json = serializer.Serialize(response); - - httpContext.Response.Headers[HeaderNames.ContentType] = "text/json"; - - return httpContext.Response.WriteAsync(json); - }); - - app.UseHealthChecks("/readiness", new HealthCheckOptions - { - Predicate = check => true, - ResponseWriter = writer - }); - - app.UseHealthChecks("/healthz", new HealthCheckOptions - { - Predicate = check => check.Tags.Contains("node"), - ResponseWriter = writer - }); - - app.UseHealthChecks("/cluster-healthz", new HealthCheckOptions - { - Predicate = check => check.Tags.Contains("cluster"), - ResponseWriter = writer - }); - - return app; - } - - public static IApplicationBuilder UseMyRobotsTxt(this IApplicationBuilder app) - { - app.Map("/robots.txt", builder => builder.UseMiddleware()); - - return app; - } - - public static void UseMyCors(this IApplicationBuilder app) - { - app.UseCors(builder => builder - .AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader()); - } - - public static void UseMyForwardingRules(this IApplicationBuilder app) - { - app.UseForwardedHeaders(new ForwardedHeadersOptions - { - ForwardedHeaders = ForwardedHeaders.XForwardedProto, - ForwardLimit = null, - RequireHeaderSymmetry = false - }); - - app.UseMiddleware(); - app.UseMiddleware(); - } - } -} diff --git a/src/Squidex/Config/Web/WebServices.cs b/src/Squidex/Config/Web/WebServices.cs deleted file mode 100644 index 97ef2eb56..000000000 --- a/src/Squidex/Config/Web/WebServices.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Squidex.Config.Domain; -using Squidex.Domain.Apps.Entities; -using Squidex.Pipeline.Plugins; -using Squidex.Pipeline.Robots; -using Squidex.Web; -using Squidex.Web.Pipeline; - -namespace Squidex.Config.Web -{ - public static class WebServices - { - public static void AddMyMvcWithPlugins(this IServiceCollection services, IConfiguration config) - { - services.AddSingletonAs(c => new ExposedValues(c.GetRequiredService>().Value, config, typeof(WebServices).Assembly)) - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsOptional(); - - services.Configure(options => - { - options.SuppressModelStateInvalidFilter = true; - }); - - services.AddMvc(options => - { - options.Filters.Add(); - options.Filters.Add(); - options.Filters.Add(); - options.Filters.Add(); - }) - .AddMyPlugins(config) - .AddMySerializers(); - - services.AddCors(); - services.AddRouting(); - } - } -} diff --git a/src/Squidex/Dockerfile b/src/Squidex/Dockerfile deleted file mode 100644 index fc00bfa8c..000000000 --- a/src/Squidex/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM microsoft/dotnet:2.2.0-aspnetcore-runtime - -WORKDIR /app - -# Copy from current directory -COPY . . - -EXPOSE 80 -EXPOSE 33333 -EXPOSE 40000 - -ENTRYPOINT ["dotnet", "Squidex.dll"] \ No newline at end of file diff --git a/src/Squidex/Pipeline/OpenApi/NSwagHelper.cs b/src/Squidex/Pipeline/OpenApi/NSwagHelper.cs deleted file mode 100644 index 1d4a3dcb0..000000000 --- a/src/Squidex/Pipeline/OpenApi/NSwagHelper.cs +++ /dev/null @@ -1,114 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.AspNetCore.Http; -using NJsonSchema; -using NSwag; - -namespace Squidex.Pipeline.OpenApi -{ - public static class NSwagHelper - { - public static readonly string SecurityDocs = LoadDocs("security"); - - public static readonly string SchemaBodyDocs = LoadDocs("schemabody"); - - public static readonly string SchemaQueryDocs = LoadDocs("schemaquery"); - - private static string LoadDocs(string name) - { - var assembly = typeof(NSwagHelper).Assembly; - - using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Docs.{name}.md")) - { - using (var streamReader = new StreamReader(resourceStream)) - { - return streamReader.ReadToEnd(); - } - } - } - - public static OpenApiDocument CreateApiDocument(HttpContext context, string appName) - { - var scheme = - string.Equals(context.Request.Scheme, "http", StringComparison.OrdinalIgnoreCase) ? - OpenApiSchema.Http : - OpenApiSchema.Https; - - var document = new OpenApiDocument - { - Schemes = new List - { - scheme - }, - Consumes = new List - { - "application/json" - }, - Produces = new List - { - "application/json" - }, - Info = new OpenApiInfo - { - Title = $"Squidex API for {appName} App" - }, - SchemaType = SchemaType.OpenApi3 - }; - - if (!string.IsNullOrWhiteSpace(context.Request.Host.Value)) - { - document.Host = context.Request.Host.Value; - } - - return document; - } - - public static void AddQuery(this OpenApiOperation operation, string name, JsonObjectType type, string description) - { - var schema = new JsonSchema { Type = type }; - - operation.AddParameter(name, schema, OpenApiParameterKind.Query, description, false); - } - - public static void AddPathParameter(this OpenApiOperation operation, string name, JsonObjectType type, string description, string format = null) - { - var schema = new JsonSchema { Type = type, Format = format }; - - operation.AddParameter(name, schema, OpenApiParameterKind.Path, description, true); - } - - public static void AddBody(this OpenApiOperation operation, string name, JsonSchema schema, string description) - { - operation.AddParameter(name, schema, OpenApiParameterKind.Body, description, true); - } - - private static void AddParameter(this OpenApiOperation operation, string name, JsonSchema schema, OpenApiParameterKind kind, string description, bool isRequired) - { - var parameter = new OpenApiParameter { Schema = schema, Name = name, Kind = kind }; - - if (!string.IsNullOrWhiteSpace(description)) - { - parameter.Description = description; - } - - parameter.IsRequired = isRequired; - - operation.Parameters.Add(parameter); - } - - public static void AddResponse(this OpenApiOperation operation, string statusCode, string description, JsonSchema schema = null) - { - var response = new OpenApiResponse { Description = description, Schema = schema }; - - operation.Responses.Add(statusCode, response); - } - } -} diff --git a/src/Squidex/Pipeline/Plugins/PluginExtensions.cs b/src/Squidex/Pipeline/Plugins/PluginExtensions.cs deleted file mode 100644 index 4608a8659..000000000 --- a/src/Squidex/Pipeline/Plugins/PluginExtensions.cs +++ /dev/null @@ -1,81 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Plugins; - -namespace Squidex.Pipeline.Plugins -{ - public static class PluginExtensions - { - public static IMvcBuilder AddMyPlugins(this IMvcBuilder mvcBuilder, IConfiguration config) - { - var pluginManager = new PluginManager(); - - var options = config.Get(); - - if (options.Plugins != null) - { - foreach (var path in options.Plugins) - { - var plugin = PluginLoaders.LoadPlugin(path); - - if (plugin != null) - { - try - { - var pluginAssembly = plugin.LoadDefaultAssembly(); - - pluginAssembly.AddParts(mvcBuilder); - pluginManager.Add(path, pluginAssembly); - } - catch (Exception ex) - { - pluginManager.LogException(path, "LoadingAssembly", ex); - } - } - else - { - pluginManager.LogException(path, "LoadingPlugin", new FileNotFoundException($"Cannot find plugin at {path}")); - } - } - } - - pluginManager.ConfigureServices(mvcBuilder.Services, config); - - mvcBuilder.Services.AddSingleton(pluginManager); - - return mvcBuilder; - } - - public static void UsePluginsBefore(this IApplicationBuilder app) - { - var pluginManager = app.ApplicationServices.GetRequiredService(); - - pluginManager.ConfigureBefore(app); - } - - public static void UsePluginsAfter(this IApplicationBuilder app) - { - var pluginManager = app.ApplicationServices.GetRequiredService(); - - pluginManager.ConfigureAfter(app); - } - - public static void UsePlugins(this IApplicationBuilder app) - { - var pluginManager = app.ApplicationServices.GetRequiredService(); - - pluginManager.Log(app.ApplicationServices.GetService()); - } - } -} diff --git a/src/Squidex/Pipeline/Plugins/PluginLoaders.cs b/src/Squidex/Pipeline/Plugins/PluginLoaders.cs deleted file mode 100644 index 5cc49e040..000000000 --- a/src/Squidex/Pipeline/Plugins/PluginLoaders.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using McMaster.NETCore.Plugins; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Plugins; -using Squidex.Web; - -namespace Squidex.Pipeline.Plugins -{ - public static class PluginLoaders - { - private static readonly Type[] SharedTypes = - { - typeof(IPlugin), - typeof(SquidexCoreModel), - typeof(SquidexCoreOperations), - typeof(SquidexEntities), - typeof(SquidexEvents), - typeof(SquidexInfrastructure), - typeof(SquidexWeb) - }; - - public static PluginLoader LoadPlugin(string pluginPath) - { - foreach (var candidate in GetPaths(pluginPath)) - { - if (candidate.Extension.Equals(".dll", StringComparison.OrdinalIgnoreCase)) - { - return PluginLoader.CreateFromAssemblyFile(candidate.FullName, PluginLoaderOptions.PreferSharedTypes); - } - - if (candidate.Extension.Equals(".json", StringComparison.OrdinalIgnoreCase)) - { - return PluginLoader.CreateFromConfigFile(candidate.FullName, SharedTypes); - } - } - - return null; - } - - private static IEnumerable GetPaths(string pluginPath) - { - var candidate = new FileInfo(Path.GetFullPath(pluginPath)); - - if (candidate.Exists) - { - yield return candidate; - } - - if (!Path.IsPathRooted(pluginPath)) - { - candidate = new FileInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), pluginPath)); - - if (candidate.Exists) - { - yield return candidate; - } - } - } - } -} diff --git a/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs b/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs deleted file mode 100644 index 92ed64692..000000000 --- a/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; -using Squidex.Infrastructure; - -namespace Squidex.Pipeline.Robots -{ - public sealed class RobotsTxtMiddleware : IMiddleware - { - private readonly RobotsTxtOptions robotsTxtOptions; - - public RobotsTxtMiddleware(IOptions robotsTxtOptions) - { - Guard.NotNull(robotsTxtOptions, nameof(robotsTxtOptions)); - - this.robotsTxtOptions = robotsTxtOptions.Value; - } - - public async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - if (CanServeRequest(context.Request) && !string.IsNullOrWhiteSpace(robotsTxtOptions.Text)) - { - context.Response.ContentType = "text/plain"; - context.Response.StatusCode = 200; - - await context.Response.WriteAsync(robotsTxtOptions.Text); - } - else - { - await next(context); - } - } - - private static bool CanServeRequest(HttpRequest request) - { - return HttpMethods.IsGet(request.Method) && string.IsNullOrEmpty(request.Path); - } - } -} diff --git a/src/Squidex/Pipeline/Squid/SquidMiddleware.cs b/src/Squidex/Pipeline/Squid/SquidMiddleware.cs deleted file mode 100644 index a9f2e305a..000000000 --- a/src/Squidex/Pipeline/Squid/SquidMiddleware.cs +++ /dev/null @@ -1,144 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace Squidex.Pipeline.Squid -{ - public sealed class SquidMiddleware - { - private readonly RequestDelegate next; - private readonly string squidHappyLG = LoadSvg("happy"); - private readonly string squidHappySM = LoadSvg("happy-sm"); - private readonly string squidSadLG = LoadSvg("sad"); - private readonly string squidSadSM = LoadSvg("sad-sm"); - - public SquidMiddleware(RequestDelegate next) - { - this.next = next; - } - - public async Task Invoke(HttpContext context) - { - var request = context.Request; - - if (request.Path.Equals("/squid.svg")) - { - var face = "sad"; - - if (request.Query.TryGetValue("face", out var faceValue) && (faceValue == "sad" || faceValue == "happy")) - { - face = faceValue; - } - - var isSad = face == "sad"; - - var title = isSad ? "OH DAMN!" : "OH YEAH!"; - - if (request.Query.TryGetValue("title", out var titleValue) && !string.IsNullOrWhiteSpace(titleValue)) - { - title = titleValue; - } - - var text = "text"; - - if (request.Query.TryGetValue("text", out var textValue) && !string.IsNullOrWhiteSpace(textValue)) - { - text = textValue; - } - - var background = isSad ? "#F5F5F9" : "#4CC159"; - - if (request.Query.TryGetValue("background", out var backgroundValue) && !string.IsNullOrWhiteSpace(backgroundValue)) - { - background = backgroundValue; - } - - var isSmall = request.Query.TryGetValue("small", out _); - - string svg; - - if (isSmall) - { - svg = isSad ? squidSadSM : squidHappySM; - } - else - { - svg = isSad ? squidSadLG : squidHappyLG; - } - - var (l1, l2, l3) = SplitText(text); - - svg = svg.Replace("{{TITLE}}", title.ToUpperInvariant()); - svg = svg.Replace("{{TEXT1}}", l1); - svg = svg.Replace("{{TEXT2}}", l2); - svg = svg.Replace("{{TEXT3}}", l3); - svg = svg.Replace("[COLOR]", background); - - context.Response.StatusCode = 200; - context.Response.ContentType = "image/svg+xml"; - context.Response.Headers["Cache-Control"] = "public, max-age=604800"; - - await context.Response.WriteAsync(svg); - } - else - { - await next(context); - } - } - - private static (string, string, string) SplitText(string text) - { - var result = new List(); - - var line = new StringBuilder(); - - foreach (var word in text.Split(' ')) - { - if (line.Length + word.Length > 17 && line.Length > 0) - { - result.Add(line.ToString()); - - line.Clear(); - } - - if (line.Length > 0) - { - line.Append(" "); - } - - line.Append(word); - } - - result.Add(line.ToString()); - - while (result.Count < 3) - { - result.Add(string.Empty); - } - - return (result[0], result[1], result[2]); - } - - private static string LoadSvg(string name) - { - var assembly = typeof(SquidMiddleware).Assembly; - - using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Pipeline.Squid.icon-{name}.svg")) - { - using (var streamReader = new StreamReader(resourceStream)) - { - return streamReader.ReadToEnd(); - } - } - } - } -} diff --git a/src/Squidex/Program.cs b/src/Squidex/Program.cs deleted file mode 100644 index 2d11f10c2..000000000 --- a/src/Squidex/Program.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.IO; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Squidex.Config; -using Squidex.Infrastructure.Log.Adapter; - -namespace Squidex -{ - public static class Program - { - public static void Main(string[] args) - { - BuildWebHost(args).Run(); - } - - public static IWebHost BuildWebHost(string[] args) => - new WebHostBuilder() - .UseKestrel(k => { k.AddServerHeader = false; }) - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIIS() - .UseStartup() - .ConfigureLogging((hostingContext, builder) => - { - builder.AddConfiguration(hostingContext.Configuration.GetSection("logging")); - builder.AddSemanticLog(); - builder.AddFilters(); - }) - .ConfigureAppConfiguration((hostContext, builder) => - { - builder.Sources.Clear(); - - builder.AddJsonFile($"appsettings.json", true); - builder.AddJsonFile($"appsettings.Custom.json", true); - builder.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", true); - - builder.AddEnvironmentVariables(); - - builder.AddCommandLine(args); - }) - .Build(); - } -} diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj deleted file mode 100644 index aabd271cc..000000000 --- a/src/Squidex/Squidex.csproj +++ /dev/null @@ -1,157 +0,0 @@ - - - InProcess - true - 2.2.0 - netcoreapp2.2 - Latest - true - 7.3 - - - - full - True - - - - - PreserveNewest - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <_DocumentationFile Include="$(DocumentationFile)" /> - - - - - - true - - - - ..\..\Squidex.ruleset - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $(NoWarn);CS1591;1591;1573;1572;NU1605;IDE0060 - - \ No newline at end of file diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs deleted file mode 100644 index b17cb7ea7..000000000 --- a/src/Squidex/WebStartup.cs +++ /dev/null @@ -1,150 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Migrate_01; -using Squidex.Areas.Api; -using Squidex.Areas.Api.Config.OpenApi; -using Squidex.Areas.Api.Controllers.Contents; -using Squidex.Areas.Api.Controllers.News; -using Squidex.Areas.Api.Controllers.UI; -using Squidex.Areas.Frontend; -using Squidex.Areas.IdentityServer; -using Squidex.Areas.IdentityServer.Config; -using Squidex.Areas.OrleansDashboard; -using Squidex.Areas.Portal; -using Squidex.Config; -using Squidex.Config.Authentication; -using Squidex.Config.Domain; -using Squidex.Config.Orleans; -using Squidex.Config.Startup; -using Squidex.Config.Web; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Diagnostics; -using Squidex.Infrastructure.Translations; -using Squidex.Pipeline.Plugins; -using Squidex.Pipeline.Robots; -using Squidex.Web; -using Squidex.Web.Pipeline; - -namespace Squidex -{ - public sealed class WebStartup - { - private readonly IConfiguration config; - private readonly IHostingEnvironment environment; - - public WebStartup(IConfiguration config, IHostingEnvironment environment) - { - this.config = config; - - this.environment = environment; - } - - public IServiceProvider ConfigureServices(IServiceCollection services) - { - services.AddHttpClient(); - services.AddLogging(); - services.AddMemoryCache(); - services.AddOptions(); - - services.AddMyMvcWithPlugins(config); - - services.AddMyAssetServices(config); - services.AddMyAuthentication(config); - services.AddMyEntitiesServices(config); - services.AddMyEventPublishersServices(config); - services.AddMyEventStoreServices(config); - services.AddMyIdentityServer(); - services.AddMyInfrastructureServices(config); - services.AddMyLoggingServices(config); - services.AddMyOpenApiSettings(); - services.AddMyMigrationServices(); - services.AddMyRuleServices(); - services.AddMySerializers(); - services.AddMyStoreServices(config); - services.AddMySubscriptionServices(config); - - services.Configure( - config.GetSection("contents")); - services.Configure( - config.GetSection("assets")); - services.Configure( - config.GetSection("translations:deepL")); - services.Configure( - config.GetSection("languages")); - services.Configure( - config.GetSection("mode")); - services.Configure( - config.GetSection("robots")); - services.Configure( - config.GetSection("healthz:gc")); - services.Configure( - config.GetSection("etags")); - services.Configure( - config.GetSection("urls")); - services.Configure( - config.GetSection("usage")); - services.Configure( - config.GetSection("rebuild")); - services.Configure( - config.GetSection("exposedConfiguration")); - services.Configure( - config.GetSection("rules")); - - services.Configure( - config.GetSection("contentsController")); - services.Configure( - config.GetSection("identity")); - services.Configure( - config.GetSection("ui")); - services.Configure( - config.GetSection("news")); - - services.AddHostedService(); - - services.AddOrleans(config, environment); - - services.AddHostedService(); - services.AddHostedService(); - services.AddHostedService(); - - return services.BuildServiceProvider(); - } - - public void Configure(IApplicationBuilder app) - { - app.ApplicationServices.LogConfiguration(); - - app.UsePluginsBefore(); - - app.UseMyHealthCheck(); - app.UseMyRobotsTxt(); - app.UseMyTracking(); - app.UseMyLocalCache(); - app.UseMyCors(); - app.UseMyForwardingRules(); - - app.ConfigureApi(); - app.ConfigurePortal(); - app.ConfigureOrleansDashboard(); - app.ConfigureIdentityServer(); - app.ConfigureFrontend(); - - app.UsePluginsAfter(); - app.UsePlugins(); - } - } -} diff --git a/src/Squidex/app-config/webpack.config.js b/src/Squidex/app-config/webpack.config.js deleted file mode 100644 index d960816dc..000000000 --- a/src/Squidex/app-config/webpack.config.js +++ /dev/null @@ -1,376 +0,0 @@ -const webpack = require('webpack'), - path = require('path'); - -const appRoot = path.resolve(__dirname, '..'); - -function root() { - var newArgs = Array.prototype.slice.call(arguments, 0); - - return path.join.apply(path, [appRoot].concat(newArgs)); -}; - -const plugins = { - // https://github.com/webpack-contrib/mini-css-extract-plugin - MiniCssExtractPlugin: require('mini-css-extract-plugin'), - // https://github.com/dividab/tsconfig-paths-webpack-plugin - TsconfigPathsPlugin: require('tsconfig-paths-webpack-plugin'), - // https://github.com/aackerman/circular-dependency-plugin - CircularDependencyPlugin: require('circular-dependency-plugin'), - // https://github.com/jantimon/html-webpack-plugin - HtmlWebpackPlugin: require('html-webpack-plugin'), - // https://webpack.js.org/plugins/terser-webpack-plugin/ - TerserPlugin: require('terser-webpack-plugin'), - // https://www.npmjs.com/package/@ngtools/webpack - NgToolsWebpack: require('@ngtools/webpack'), - // https://github.com/NMFR/optimize-css-assets-webpack-plugin - OptimizeCSSAssetsPlugin: require("optimize-css-assets-webpack-plugin"), - // https://github.com/jrparish/tslint-webpack-plugin - TsLintPlugin: require('tslint-webpack-plugin') -}; - -module.exports = function (env) { - const isDevServer = path.basename(require.main.filename) === 'webpack-dev-server.js'; - const isProduction = env && env.production; - const isTests = env && env.target === 'tests'; - const isCoverage = env && env.coverage; - const isAot = isProduction; - - const config = { - mode: isProduction ? 'production' : 'development', - - /** - * Source map for Karma from the help of karma-sourcemap-loader & karma-webpack. - * - * See: https://webpack.js.org/configuration/devtool/ - */ - devtool: isProduction ? false : 'inline-source-map', - - /** - * Options affecting the resolving of modules. - * - * See: https://webpack.js.org/configuration/resolve/ - */ - resolve: { - /** - * An array of extensions that should be used to resolve modules. - * - * See: https://webpack.js.org/configuration/resolve/#resolve-extensions - */ - extensions: ['.ts', '.js', '.mjs', '.css', '.scss'], - modules: [ - root('app'), - root('app', 'theme'), - root('node_modules') - ], - - plugins: [ - new plugins.TsconfigPathsPlugin() - ] - }, - - /** - * Options affecting the normal modules. - * - * See: https://webpack.js.org/configuration/module/ - */ - module: { - /** - * An array of Rules which are matched to requests when modules are created. - * - * See: https://webpack.js.org/configuration/module/#module-rules - */ - rules: [{ - test: /\.mjs$/, - type: "javascript/auto", - include: [/node_modules/] - }, { - test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/, - parser: { system: true }, - include: [/node_modules/] - }, { - test: /\.js\.flow$/, - use: [{ - loader: 'ignore-loader' - }], - include: [/node_modules/] - }, { - test: /\.map$/, - use: [{ - loader: 'ignore-loader' - }], - include: [/node_modules/] - }, { - test: /\.d\.ts$/, - use: [{ - loader: 'ignore-loader' - }], - include: [/node_modules/] - }, { - test: /\.(woff|woff2|ttf|eot)(\?.*$|$)/, - use: [{ - loader: 'file-loader?name=[name].[hash].[ext]', - options: { - outputPath: 'assets', - /* - * Use custom public path as ./ is not supported by fonts. - */ - publicPath: isDevServer ? undefined : 'assets' - } - }] - }, { - test: /\.(png|jpe?g|gif|svg|ico)(\?.*$|$)/, - use: [{ - loader: 'file-loader?name=[name].[hash].[ext]', - options: { - outputPath: 'assets' - } - }] - }, { - test: /\.css$/, - use: [ - plugins.MiniCssExtractPlugin.loader, - { - loader: 'css-loader' - }] - }, { - test: /\.scss$/, - use: [{ - loader: 'raw-loader' - }, { - loader: 'sass-loader', options: { - sassOptions: { - includePaths: [root('app', 'theme')] - } - } - }], - exclude: root('app', 'theme') - }] - }, - - plugins: [ - new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)fesm5/, root('./app'), {}), - new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/), - - /** - * Puts each bundle into a file and appends the hash of the file to the path. - * - * See: https://github.com/webpack-contrib/mini-css-extract-plugin - */ - new plugins.MiniCssExtractPlugin('[name].css'), - - new webpack.LoaderOptionsPlugin({ - options: { - htmlLoader: { - /** - * Define the root for images, so that we can use absolute urls. - * - * See: https://github.com/webpack/html-loader#Advanced_Options - */ - root: root('app', 'images') - }, - context: '/' - } - }), - - /** - * Detect circular dependencies in app. - * - * See: https://github.com/aackerman/circular-dependency-plugin - */ - new plugins.CircularDependencyPlugin({ - exclude: /([\\\/]node_modules[\\\/])|(ngfactory\.js$)/, - // Add errors to webpack instead of warnings - failOnError: true - }), - ], - - devServer: { - headers: { - 'Access-Control-Allow-Origin': '*' - }, - historyApiFallback: true - } - }; - - if (!isTests) { - /** - * The entry point for the bundle. Our Angular app. - * - * See: https://webpack.js.org/configuration/entry-context/ - */ - config.entry = { - 'shims': './app/shims.ts', - 'app': './app/app.ts' - }; - - if (isProduction) { - config.output = { - /** - * The output directory as absolute path (required). - * - * See: https://webpack.js.org/configuration/output/#output-path - */ - path: root('wwwroot/build/'), - - publicPath: './build/', - - /** - * Specifies the name of each output file on disk. - * - * See: https://webpack.js.org/configuration/output/#output-filename - */ - filename: '[name].js', - - /** - * The filename of non-entry chunks as relative path inside the output.path directory. - * - * See: https://webpack.js.org/configuration/output/#output-chunkfilename - */ - chunkFilename: '[id].[hash].chunk.js' - }; - } else { - config.output = { - filename: '[name].js', - - /** - * Set the public path, because we are running the website from another port (5000). - */ - publicPath: 'http://localhost:3000/' - }; - } - - config.plugins.push( - new plugins.HtmlWebpackPlugin({ - hash: true, - chunks: ['shims', 'app'], - chunksSortMode: 'manual', - template: 'wwwroot/index.html' - }) - ); - - config.plugins.push( - new plugins.HtmlWebpackPlugin({ - template: 'wwwroot/_theme.html', hash: true, chunksSortMode: 'none', filename: 'theme.html' - }) - ); - - config.plugins.push( - new plugins.TsLintPlugin({ - files: ['./app/**/*.ts'], - /** - * Path to a configuration file. - */ - config: root('tslint.json'), - /** - * Wait for linting and fail the build when linting error occur. - */ - waitForLinting: isProduction - }) - ); - } - - if (!isCoverage) { - config.plugins.push( - new plugins.NgToolsWebpack.AngularCompilerPlugin({ - directTemplateLoading: true, - entryModule: 'app/app.module#AppModule', - sourceMap: !isProduction, - skipCodeGeneration: !isAot, - tsConfigPath: './tsconfig.json' - }) - ); - } - - if (isProduction) { - config.optimization = { - minimizer: [ - new plugins.TerserPlugin({ - terserOptions: { - compress: true, - ecma: 5, - mangle: true, - output: { - comments: false - }, - safari10: true - }, - extractComments: true - }), - - new plugins.OptimizeCSSAssetsPlugin({}) - ] - }; - - config.performance = { - hints: false - }; - } - - if (isCoverage) { - // Do not instrument tests. - config.module.rules.push({ - test: /\.ts$/, - use: [{ - loader: 'ts-loader' - }], - include: [/\.(e2e|spec)\.ts$/], - }); - - // Use instrument loader for all normal files. - config.module.rules.push({ - test: /\.ts$/, - use: [{ - loader: 'istanbul-instrumenter-loader?esModules=true' - }, { - loader: 'ts-loader' - }], - exclude: [/\.(e2e|spec)\.ts$/] - }); - } else { - config.module.rules.push({ - test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, - use: [{ - loader: plugins.NgToolsWebpack.NgToolsLoader - }] - }) - } - - if (isProduction) { - config.module.rules.push({ - test: /\.scss$/, - /* - * Extract the content from a bundle to a file. - * - * See: https://github.com/webpack-contrib/extract-text-webpack-plugin - */ - use: [ - plugins.MiniCssExtractPlugin.loader, - { - loader: 'css-loader' - }, { - loader: 'sass-loader' - }], - /* - * Do not include component styles. - */ - include: root('app', 'theme'), - }); - } else { - config.module.rules.push({ - test: /\.scss$/, - use: [{ - loader: 'style-loader' - }, { - loader: 'css-loader' - }, { - loader: 'sass-loader?sourceMap' - }], - /* - * Do not include component styles. - */ - include: root('app', 'theme') - }); - } - - return config; -}; \ No newline at end of file diff --git a/src/Squidex/package-lock.json b/src/Squidex/package-lock.json deleted file mode 100644 index e2495cb60..000000000 --- a/src/Squidex/package-lock.json +++ /dev/null @@ -1,17154 +0,0 @@ -{ - "name": "squidex", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@angular-devkit/build-optimizer": { - "version": "0.803.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.803.8.tgz", - "integrity": "sha512-UiMxl1wI3acqIoRkC0WA0qpab+ni6SlCaB4UIwfD1H/FdzU80P04AIUuJS7StxjbwVkVtA05kcfgmqzP8yBMVg==", - "dev": true, - "requires": { - "loader-utils": "1.2.3", - "source-map": "0.7.3", - "tslib": "1.10.0", - "typescript": "3.5.3", - "webpack-sources": "1.4.3" - }, - "dependencies": { - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - } - } - }, - "@angular-devkit/core": { - "version": "8.3.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-8.3.8.tgz", - "integrity": "sha512-HwlMRr6qANwhOJS+5rGgQ2lmP4nj2C4cbUc0LlA09Cdbq0RnDquUFVqHF6h81FUKFW1D5qDehWYHNOVq8+gTkQ==", - "dev": true, - "requires": { - "ajv": "6.10.2", - "fast-json-stable-stringify": "2.0.0", - "magic-string": "0.25.3", - "rxjs": "6.4.0", - "source-map": "0.7.3" - }, - "dependencies": { - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "magic-string": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.3.tgz", - "integrity": "sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA==", - "dev": true, - "requires": { - "sourcemap-codec": "^1.4.4" - } - }, - "rxjs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", - "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - } - } - }, - "@angular/animations": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-8.2.9.tgz", - "integrity": "sha512-l30AF0d9P5okTPM1wieUHgcnDyGSNvyaBcxXSOkT790wAP2v5zs7VrKq9Lm+ICu4Nkx07KrOr5XLUHhqsg3VXA==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/cdk": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-8.2.3.tgz", - "integrity": "sha512-ZwO5Sn720RA2YvBqud0JAHkZXjmjxM0yNzCO8RVtRE9i8Gl26Wk0j0nQeJkVm4zwv2QO8MwbKUKGTMt8evsokA==", - "requires": { - "parse5": "^5.0.0", - "tslib": "^1.7.1" - } - }, - "@angular/common": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-8.2.9.tgz", - "integrity": "sha512-76WDU1USlI5vAzqCJ3gxCQGuu57aJEggNk/xoWmQEXipiFTFBh2wSKn/dE6Txr/q3COTPIcrmb9OCeal5kQPIA==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/compiler": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-8.2.9.tgz", - "integrity": "sha512-oQho19DnOhEDNerCOGuGK95tcZ2oy4dSA5SykJmmniRnZzPM2++bJD32qJehXHy1K+3hv2zN9x7HPhqT3ljT6g==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/compiler-cli": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-8.2.9.tgz", - "integrity": "sha512-tqGBKPf3SRYNEGGJbmjom//U/eAjnecDhGUw6o+VkYE/wxYd9pPcLmcEwwyXBpIPJAsN8RsjTikPuH0gcNE8bw==", - "dev": true, - "requires": { - "canonical-path": "1.0.0", - "chokidar": "^2.1.1", - "convert-source-map": "^1.5.1", - "dependency-graph": "^0.7.2", - "magic-string": "^0.25.0", - "minimist": "^1.2.0", - "reflect-metadata": "^0.1.2", - "source-map": "^0.6.1", - "tslib": "^1.9.0", - "yargs": "13.1.0" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - } - } - }, - "@angular/core": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-8.2.9.tgz", - "integrity": "sha512-GpHAuLOlN9iioELCQBmAsjETTUCyFgVUI3LXwh3e63jnpd+ZuuZcZbjfTYhtgYVNMetn7cVEO6p88eb7qvpUWQ==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/forms": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-8.2.9.tgz", - "integrity": "sha512-kAdBuApC9PPOdPI8BmNhxCraAkXGbX/PkVan8pQ5xdumvgGqvVjbJvLaUSbJROPtgCRlQyiEDrHFd4gk/WU76A==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/http": { - "version": "7.2.15", - "resolved": "https://registry.npmjs.org/@angular/http/-/http-7.2.15.tgz", - "integrity": "sha512-TR7PEdmLWNIre3Zn8lvyb4lSrvPUJhKLystLnp4hBMcWsJqq5iK8S3bnlR4viZ9HMlf7bW7+Hm4SI6aB3tdUtw==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/platform-browser": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-8.2.9.tgz", - "integrity": "sha512-k3aNZy0OTqGn7HlHHV52QF6ZAP/VlQhWGD2u5e1dWIWMq39kdkdSCNu5tiuAf5hIzMBiSQ0tjnuVWA4MuDBYIQ==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/platform-browser-dynamic": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-8.2.9.tgz", - "integrity": "sha512-GbE4TUy4n/a8yp8fLWwdG/QnjUPZZ8VufItZ7GvOpoyknzegvka111dLctvMoPzSAsrKyShL6cryuyDC5PShUA==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/platform-server": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-8.2.9.tgz", - "integrity": "sha512-rr6h82+DdUGhpsF3WT3eLk5itjZDXe7SiNtRGHkPj+yTyFAxuTKA3cX0N7LWsGGIFax+s1vQhMreV4YcyHKGPQ==", - "requires": { - "domino": "^2.1.2", - "tslib": "^1.9.0", - "xhr2": "^0.1.4" - } - }, - "@angular/router": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-8.2.9.tgz", - "integrity": "sha512-4P60CWNB/jxGjDBEuYN0Jobt76QlebAQeFBTDswRVwRlq/WJT4QhL3a8AVIRsHn9bQII0LUt/ZQBBPxn7h9lSA==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/generator": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz", - "integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==", - "dev": true, - "requires": { - "@babel/types": "^7.5.5", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "dev": true, - "requires": { - "@babel/types": "^7.4.4" - } - }, - "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", - "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==", - "dev": true - }, - "@babel/template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", - "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4" - } - }, - "@babel/traverse": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz", - "integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.5.5", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.5.5", - "@babel/types": "^7.5.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz", - "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - }, - "dependencies": { - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - } - } - }, - "@ngtools/webpack": { - "version": "8.3.8", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-8.3.8.tgz", - "integrity": "sha512-jLN4/Abue+Ro/K2SF0TpHOXnFHGuaHQ4aL6QG++moZXavBxRdc2E+PDjtuaMaS1llLHs5C5GX+Ve9ueEFhWoeQ==", - "dev": true, - "requires": { - "@angular-devkit/core": "8.3.8", - "enhanced-resolve": "4.1.0", - "rxjs": "6.4.0", - "tree-kill": "1.2.1", - "webpack-sources": "1.4.3" - }, - "dependencies": { - "rxjs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", - "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - } - } - }, - "@types/core-js": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@types/core-js/-/core-js-2.5.2.tgz", - "integrity": "sha512-+NPqjXgyA02xTHKJDeDca9u8Zr42ts6jhdND4C3PrPeQ35RJa0dmfAedXW7a9K4N1QcBbuWI1nSfGK4r1eVFCQ==", - "dev": true - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", - "dev": true - }, - "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/jasmine": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.4.2.tgz", - "integrity": "sha512-SaSSGOzwUnBEn64c+HTyVTJhRf8F1CXZLnxYx2ww3UrgGBmEEw38RSux2l3fYiT9brVLP67DU5omWA6V9OHI5Q==", - "dev": true - }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true - }, - "@types/marked": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.6.5.tgz", - "integrity": "sha512-6kBKf64aVfx93UJrcyEZ+OBM5nGv4RLsI6sR1Ar34bpgvGVRoyTgpxn4ZmtxOM5aDTAaaznYuYUH8bUX3Nk3YA==", - "dev": true - }, - "@types/mersenne-twister": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@types/mersenne-twister/-/mersenne-twister-1.1.2.tgz", - "integrity": "sha512-7KMIfSkMpaVExbzJRLUXHMO4hkFWbbspHPREk8I6pBxiNN+3+l6eAEClMCIPIo2KjCkR0rjYfXppr6+wKdTwpA==", - "dev": true - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/mousetrap": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.0.tgz", - "integrity": "sha512-Jn2cF8X6RAMiSmJaATGjf2r3GzIfpZQpvnQhKprQ5sAbMaNXc7hc9sA2XHdMl3bEMEQhTV79JVW7n4Pgg7sjtg==", - "dev": true - }, - "@types/node": { - "version": "12.7.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.11.tgz", - "integrity": "sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw==", - "dev": true - }, - "@types/prop-types": { - "version": "15.7.3", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", - "dev": true - }, - "@types/q": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", - "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", - "dev": true - }, - "@types/react": { - "version": "16.9.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.5.tgz", - "integrity": "sha512-jQ12VMiFOWYlp+j66dghOWcmDDwhca0bnlcTxS4Qz/fh5gi6wpaZDthPEu/Gc/YlAuO87vbiUXL8qKstFvuOaA==", - "dev": true, - "requires": { - "@types/prop-types": "*", - "csstype": "^2.2.0" - } - }, - "@types/react-dom": { - "version": "16.9.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.1.tgz", - "integrity": "sha512-1S/akvkKr63qIUWVu5IKYou2P9fHLb/P2VAwyxVV85JGaGZTcUniMiTuIqM3lXFB25ej6h+CYEQ27ERVwi6eGA==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, - "@types/sortablejs": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.7.2.tgz", - "integrity": "sha512-yIxpbtlfhaFi2QyuUK54XcmzDWZf5i11CgTrMO4Vh+sKKZthonizkTcqhADeHdngDNTDVUCYfIcfIvpZRAZY+A==", - "dev": true - }, - "@webassemblyjs/ast": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", - "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", - "dev": true, - "requires": { - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", - "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", - "dev": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", - "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", - "dev": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", - "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", - "dev": true - }, - "@webassemblyjs/helper-code-frame": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", - "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", - "dev": true, - "requires": { - "@webassemblyjs/wast-printer": "1.8.5" - } - }, - "@webassemblyjs/helper-fsm": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", - "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", - "dev": true - }, - "@webassemblyjs/helper-module-context": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", - "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "mamacro": "^0.0.3" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", - "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", - "dev": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", - "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", - "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", - "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", - "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", - "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", - "dev": true - }, - "@webassemblyjs/wasm-edit": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", - "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/helper-wasm-section": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-opt": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5", - "@webassemblyjs/wast-printer": "1.8.5" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", - "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/ieee754": "1.8.5", - "@webassemblyjs/leb128": "1.8.5", - "@webassemblyjs/utf8": "1.8.5" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", - "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", - "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-api-error": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/ieee754": "1.8.5", - "@webassemblyjs/leb128": "1.8.5", - "@webassemblyjs/utf8": "1.8.5" - } - }, - "@webassemblyjs/wast-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", - "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/floating-point-hex-parser": "1.8.5", - "@webassemblyjs/helper-api-error": "1.8.5", - "@webassemblyjs/helper-code-frame": "1.8.5", - "@webassemblyjs/helper-fsm": "1.8.5", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", - "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5", - "@xtuc/long": "4.2.2" - } - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "dev": true, - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", - "dev": true - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "^3.0.4" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", - "dev": true - }, - "aggregate-error": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", - "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "dependencies": { - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - } - } - }, - "ajv": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", - "integrity": "sha1-06/3jpJ3VJdx2vAWTP9ISCt1T8Y=", - "dev": true, - "requires": { - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0", - "uri-js": "^3.0.2" - } - }, - "ajv-errors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.0.tgz", - "integrity": "sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk=", - "dev": true - }, - "ajv-keywords": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", - "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", - "dev": true - }, - "alphanum-sort": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", - "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", - "dev": true - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "angular2-chartjs": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/angular2-chartjs/-/angular2-chartjs-0.5.1.tgz", - "integrity": "sha512-bxEVxVEv7llMcgwuc9jlc5KmuOEngT7ZlUyCddmsXwQQAahrTeNgFJ1Nc1SVQnq2fl2d8efh6m70DqF5beiA+A==", - "requires": { - "chart.js": "^2.3.0" - } - }, - "ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", - "dev": true - }, - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", - "dev": true - }, - "ansi-html": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", - "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - }, - "dependencies": { - "color-convert": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", - "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", - "dev": true, - "requires": { - "color-name": "1.1.1" - } - }, - "color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", - "dev": true - } - } - }, - "anymatch": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", - "dev": true, - "requires": { - "micromatch": "^2.1.5", - "normalize-path": "^2.0.0" - } - }, - "app-root-path": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz", - "integrity": "sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA==", - "dev": true - }, - "append-transform": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", - "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", - "dev": true, - "requires": { - "default-require-extensions": "^2.0.0" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "aria-query": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", - "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7", - "commander": "^2.11.0" - } - }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-filter": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", - "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", - "dev": true - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, - "array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true - }, - "array-map": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", - "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", - "dev": true - }, - "array-reduce": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", - "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "arraybuffer.slice": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "dev": true, - "requires": { - "object-assign": "^4.1.1", - "util": "0.10.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "ast-types": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", - "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=", - "dev": true - }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", - "dev": true - }, - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - }, - "dependencies": { - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - } - } - }, - "async-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true - }, - "async-foreach": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", - "dev": true - }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "axobject-query": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", - "integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7" - } - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": true, - "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "1.3.0", - "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-polyfill": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", - "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", - "requires": { - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "regenerator-runtime": "^0.10.5" - }, - "dependencies": { - "core-js": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", - "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" - } - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - }, - "dependencies": { - "core-js": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", - "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - } - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true - }, - "backo2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", - "dev": true - }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" - }, - "base64id": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", - "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", - "dev": true - }, - "batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "dev": true, - "requires": { - "callsite": "1.0.0" - } - }, - "big.js": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", - "dev": true - }, - "binary-extensions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", - "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", - "dev": true - }, - "blob": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", - "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", - "dev": true - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "bluebird": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.0.tgz", - "integrity": "sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg==", - "dev": true - }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true - }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "dev": true, - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - } - }, - "bonjour": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", - "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", - "dev": true, - "requires": { - "array-flatten": "^2.1.0", - "deep-equal": "^1.0.1", - "dns-equal": "^1.0.0", - "dns-txt": "^2.0.2", - "multicast-dns": "^6.0.1", - "multicast-dns-service-types": "^1.1.0" - } - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true - }, - "bootstrap": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz", - "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, - "requires": { - "expand-range": "^1.8.1", - "preserve": "^0.2.0", - "repeat-element": "^1.1.2" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "dev": true, - "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "requires": { - "pako": "~1.0.5" - } - }, - "browserslist": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.0.tgz", - "integrity": "sha512-9rGNDtnj+HaahxiVV38Gn8n8Lr8REKsel68v1sPFfIGEK6uSXTY3h9acgiT1dZVtOOUtifo/Dn8daDQ5dUgVsA==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30000989", - "electron-to-chromium": "^1.3.247", - "node-releases": "^1.1.29" - } - }, - "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "dev": true, - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "dev": true - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", - "dev": true - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "buffer-indexof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", - "dev": true - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", - "dev": true - }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true - }, - "cacache": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz", - "integrity": "sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w==", - "dev": true, - "requires": { - "chownr": "^1.1.2", - "figgy-pudding": "^3.5.1", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.2", - "infer-owner": "^1.0.4", - "lru-cache": "^5.1.1", - "minipass": "^3.0.0", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "p-map": "^3.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^2.7.1", - "ssri": "^7.0.0", - "unique-filename": "^1.1.1" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", - "dev": true - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "dev": true, - "requires": { - "callsites": "^2.0.0" - }, - "dependencies": { - "callsites": { - "version": "2.0.0", - "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - } - } - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "^0.2.0" - } - }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", - "dev": true - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "camel-case": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", - "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", - "dev": true, - "requires": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - } - } - }, - "caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "caniuse-lite": { - "version": "1.0.30000998", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000998.tgz", - "integrity": "sha512-8Tj5sPZR9kMHeDD9SZXIVr5m9ofufLLCG2Y4QwQrH18GIwG+kCc+zYdlR036ZRkuKjVVetyxeAgGA1xF7XdmzQ==", - "dev": true - }, - "canonical-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/canonical-path/-/canonical-path-1.0.0.tgz", - "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chart.js": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.7.2.tgz", - "integrity": "sha512-90wl3V9xRZ8tnMvMlpcW+0Yg13BelsGS9P9t0ClaDxv/hdypHDr/YAGf+728m11P5ljwyB0ZHfPKCapZFqSqYA==", - "requires": { - "chartjs-color": "^2.1.0", - "moment": "^2.10.2" - } - }, - "chartjs-color": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.2.0.tgz", - "integrity": "sha1-hKL7dVeH7YXDndbdjHsdiEKbrq4=", - "requires": { - "chartjs-color-string": "^0.5.0", - "color-convert": "^0.5.3" - } - }, - "chartjs-color-string": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz", - "integrity": "sha512-amWNvCOXlOUYxZVDSa0YOab5K/lmEhbFNKI55PWc4mlv28BDzA7zaoQTGxSBgJMHIW+hGX8YUrvw/FH4LyhwSQ==", - "requires": { - "color-name": "^1.0.0" - } - }, - "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", - "dev": true, - "requires": { - "anymatch": "^1.3.0", - "async-each": "^1.0.0", - "fsevents": "^1.0.0", - "glob-parent": "^2.0.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^2.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0" - } - }, - "chownr": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", - "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", - "dev": true - }, - "chrome-trace-event": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", - "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "circular-dependency-plugin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz", - "integrity": "sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw==", - "dev": true - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "clean-css": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", - "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", - "dev": true, - "requires": { - "source-map": "0.5.x" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "^1.0.1" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "dev": true, - "requires": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "codelyzer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-5.1.2.tgz", - "integrity": "sha512-1z7mtpwxcz5uUqq0HLO0ifj/tz2dWEmeaK+8c5TEZXAwwVxrjjg0118ODCOCCOcpfYaaEHxStNCaWVYo9FUPXw==", - "dev": true, - "requires": { - "app-root-path": "^2.2.1", - "aria-query": "^3.0.0", - "axobject-query": "^2.0.2", - "css-selector-tokenizer": "^0.7.1", - "cssauron": "^1.4.0", - "damerau-levenshtein": "^1.0.4", - "semver-dsl": "^1.0.1", - "source-map": "^0.5.7", - "sprintf-js": "^1.1.2" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", - "dev": true - } - } - }, - "codemirror": { - "version": "5.49.0", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.49.0.tgz", - "integrity": "sha512-Hyzr0HToBdZpLBN9dYFO/KlJAsKH37/cXVHPAqa+imml0R92tb9AkmsvjnXL+SluEvjjdfkDgRjc65NG5jnMYA==" - }, - "codemirror-graphql": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-0.8.3.tgz", - "integrity": "sha512-ZipSnPXFKDMThfvfTKTAt1dQmuGctVNann8hTZg6017+vwOcGpIqCuQIZLRDw/Y3zZfCyydRARHgbSydSCXpow==", - "requires": { - "graphql-language-service-interface": "^1.3.2", - "graphql-language-service-parser": "^1.2.2" - } - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", - "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", - "dev": true, - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - }, - "dependencies": { - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - } - } - }, - "color-convert": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", - "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "color-string": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", - "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", - "dev": true, - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true - }, - "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.16.0.tgz", - "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "compare-versions": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.5.1.tgz", - "integrity": "sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg==", - "dev": true - }, - "component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", - "dev": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "component-inherit": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", - "dev": true - }, - "compressible": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", - "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", - "dev": true, - "requires": { - "mime-db": ">= 1.40.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "dependencies": { - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", - "dev": true, - "requires": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - } - }, - "connect-history-api-fallback": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", - "dev": true - }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "dev": true, - "requires": { - "date-now": "^0.1.4" - } - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", - "dev": true - }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "dev": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true - }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", - "dev": true - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "copy-to-clipboard": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz", - "integrity": "sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==", - "requires": { - "toggle-selection": "^1.0.6" - } - }, - "core-js": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.2.1.tgz", - "integrity": "sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - }, - "dependencies": { - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - } - } - }, - "cpx": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/cpx/-/cpx-1.5.0.tgz", - "integrity": "sha1-GFvgGFEdhycN7czCkxceN2VauI8=", - "dev": true, - "requires": { - "babel-runtime": "^6.9.2", - "chokidar": "^1.6.0", - "duplexer": "^0.1.1", - "glob": "^7.0.5", - "glob2base": "^0.0.12", - "minimatch": "^3.0.2", - "mkdirp": "^0.5.1", - "resolve": "^1.1.7", - "safe-buffer": "^5.0.1", - "shell-quote": "^1.6.1", - "subarg": "^1.0.0" - } - }, - "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.0.0" - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-fetch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.2.tgz", - "integrity": "sha1-pH/09/xxLauo9qaVoRyUhEDUVyM=", - "requires": { - "node-fetch": "2.1.2", - "whatwg-fetch": "2.0.4" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "crypto-js": { - "version": "3.1.9-1", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", - "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" - }, - "css-color-names": { - "version": "0.0.4", - "resolved": "http://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", - "dev": true - }, - "css-declaration-sorter": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", - "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", - "dev": true, - "requires": { - "postcss": "^7.0.1", - "timsort": "^0.3.0" - } - }, - "css-loader": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.2.0.tgz", - "integrity": "sha512-QTF3Ud5H7DaZotgdcJjGMvyDj5F3Pn1j/sC6VBEOVp94cbwqyIBdcs/quzj4MC1BKQSrTpQznegH/5giYbhnCQ==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "cssesc": "^3.0.0", - "icss-utils": "^4.1.1", - "loader-utils": "^1.2.3", - "normalize-path": "^3.0.0", - "postcss": "^7.0.17", - "postcss-modules-extract-imports": "^2.0.0", - "postcss-modules-local-by-default": "^3.0.2", - "postcss-modules-scope": "^2.1.0", - "postcss-modules-values": "^3.0.0", - "postcss-value-parser": "^4.0.0", - "schema-utils": "^2.0.0" - }, - "dependencies": { - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "schema-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", - "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true, - "requires": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" - } - }, - "css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", - "dev": true - }, - "css-selector-tokenizer": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", - "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==", - "dev": true, - "requires": { - "cssesc": "^0.1.0", - "fastparse": "^1.1.1", - "regexpu-core": "^1.0.0" - } - }, - "css-tree": { - "version": "1.0.0-alpha.33", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.33.tgz", - "integrity": "sha512-SPt57bh5nQnpsTBsx/IXbO14sRc9xXu5MtMAVuo0BaQQmyf0NupNPPSoMaqiAF5tDFafYsTkfeH4Q/HCKXkg4w==", - "dev": true, - "requires": { - "mdn-data": "2.0.4", - "source-map": "^0.5.3" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "css-unit-converter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz", - "integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=", - "dev": true - }, - "css-what": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", - "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", - "dev": true - }, - "cssauron": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", - "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", - "dev": true, - "requires": { - "through": "X.X.X" - } - }, - "cssesc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", - "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", - "dev": true - }, - "cssnano": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", - "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", - "dev": true, - "requires": { - "cosmiconfig": "^5.0.0", - "cssnano-preset-default": "^4.0.7", - "is-resolvable": "^1.0.0", - "postcss": "^7.0.0" - } - }, - "cssnano-preset-default": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", - "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", - "dev": true, - "requires": { - "css-declaration-sorter": "^4.0.1", - "cssnano-util-raw-cache": "^4.0.1", - "postcss": "^7.0.0", - "postcss-calc": "^7.0.1", - "postcss-colormin": "^4.0.3", - "postcss-convert-values": "^4.0.1", - "postcss-discard-comments": "^4.0.2", - "postcss-discard-duplicates": "^4.0.2", - "postcss-discard-empty": "^4.0.1", - "postcss-discard-overridden": "^4.0.1", - "postcss-merge-longhand": "^4.0.11", - "postcss-merge-rules": "^4.0.3", - "postcss-minify-font-values": "^4.0.2", - "postcss-minify-gradients": "^4.0.2", - "postcss-minify-params": "^4.0.2", - "postcss-minify-selectors": "^4.0.2", - "postcss-normalize-charset": "^4.0.1", - "postcss-normalize-display-values": "^4.0.2", - "postcss-normalize-positions": "^4.0.2", - "postcss-normalize-repeat-style": "^4.0.2", - "postcss-normalize-string": "^4.0.2", - "postcss-normalize-timing-functions": "^4.0.2", - "postcss-normalize-unicode": "^4.0.1", - "postcss-normalize-url": "^4.0.1", - "postcss-normalize-whitespace": "^4.0.2", - "postcss-ordered-values": "^4.1.2", - "postcss-reduce-initial": "^4.0.3", - "postcss-reduce-transforms": "^4.0.2", - "postcss-svgo": "^4.0.2", - "postcss-unique-selectors": "^4.0.1" - } - }, - "cssnano-util-get-arguments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", - "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", - "dev": true - }, - "cssnano-util-get-match": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", - "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", - "dev": true - }, - "cssnano-util-raw-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", - "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "cssnano-util-same-parent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", - "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", - "dev": true - }, - "csso": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", - "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==", - "dev": true, - "requires": { - "css-tree": "1.0.0-alpha.29" - }, - "dependencies": { - "css-tree": { - "version": "1.0.0-alpha.29", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz", - "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==", - "dev": true, - "requires": { - "mdn-data": "~1.1.0", - "source-map": "^0.5.3" - } - }, - "mdn-data": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz", - "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "csstype": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz", - "integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==", - "dev": true - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, - "custom-event": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", - "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", - "dev": true - }, - "cyclist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", - "dev": true - }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true, - "requires": { - "es5-ext": "^0.10.9" - } - }, - "damerau-levenshtein": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz", - "integrity": "sha512-CBCRqFnpu715iPmw1KrdOrzRqbdFwQTwAWyyyYS42+iAgHCuXZ+/TdMgQkUENPomxEz9z1BEzuQU2Xw0kUuAgA==", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "date-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", - "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", - "dev": true - }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.0.tgz", - "integrity": "sha512-ZbfWJq/wN1Z273o7mUSjILYqehAktR2NVoSrOukDkU9kg2v/Uv89yU4Cvz8seJeAmtN5oqiefKq8FPuXOboqLw==", - "dev": true, - "requires": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - }, - "dependencies": { - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - } - } - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "deepmerge": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.1.1.tgz", - "integrity": "sha512-urQxA1smbLZ2cBbXbaYObM1dJ82aJ2H57A1C/Kklfh/ZN1bgH4G/n5KWhdNfOK11W98gqZfyYj7W4frJJRwA2w==", - "dev": true - }, - "default-gateway": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", - "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "ip-regex": "^2.1.0" - } - }, - "default-require-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", - "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", - "dev": true, - "requires": { - "strip-bom": "^3.0.0" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "del": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", - "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "globby": "^6.1.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "pify": "^4.0.1", - "rimraf": "^2.6.3" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, - "dependency-graph": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.7.2.tgz", - "integrity": "sha512-KqtH4/EZdtdfWX0p6MGP9jljvxSY6msy/pRUD4jgNwVpv3v1QmNLlsB3LDSSUg79BRVSn7jI1QPRtArGABovAQ==", - "dev": true - }, - "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true - }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", - "dev": true - }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "detect-node": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", - "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", - "dev": true - }, - "di": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", - "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", - "dev": true - }, - "diff": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", - "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", - "dev": true - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", - "dev": true - }, - "dns-packet": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", - "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", - "dev": true, - "requires": { - "ip": "^1.1.0", - "safe-buffer": "^5.0.1" - } - }, - "dns-txt": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", - "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", - "dev": true, - "requires": { - "buffer-indexof": "^1.0.0" - } - }, - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" - } - }, - "dom-converter": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz", - "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=", - "dev": true, - "requires": { - "utila": "~0.3" - }, - "dependencies": { - "utila": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", - "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", - "dev": true - } - } - }, - "dom-serialize": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", - "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", - "dev": true, - "requires": { - "custom-event": "~1.0.0", - "ent": "~2.2.0", - "extend": "^3.0.0", - "void-elements": "^2.0.0" - } - }, - "dom-serializer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", - "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", - "dev": true, - "requires": { - "domelementtype": "~1.1.1", - "entities": "~1.1.1" - }, - "dependencies": { - "domelementtype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", - "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", - "dev": true - } - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true - }, - "domelementtype": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", - "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", - "dev": true - }, - "domhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz", - "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "domino": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.3.tgz", - "integrity": "sha512-EwjTbUv1Q/RLQOdn9k7ClHutrQcWGsfXaRQNOnM/KgK4xDBoLFEcIRFuBSxAx13Vfa63X029gXYrNFrSy+DOSg==" - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", - "dev": true - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true - }, - "electron-to-chromium": { - "version": "1.3.273", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.273.tgz", - "integrity": "sha512-0kUppiHQvHEENHh+nTtvTt4eXMwcPyWmMaj73GPrSEm3ldKhmmHuOH6IjrmuW6YmyS/fpXcLvMQLNVpqRhpNWw==", - "dev": true - }, - "elliptic": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz", - "integrity": "sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg==", - "dev": true, - "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "engine.io": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz", - "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==", - "dev": true, - "requires": { - "accepts": "~1.3.4", - "base64id": "1.0.0", - "cookie": "0.3.1", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.0", - "ws": "~3.3.1" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "engine.io-client": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", - "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", - "dev": true, - "requires": { - "component-emitter": "1.2.1", - "component-inherit": "0.0.3", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.1", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "ws": "~3.3.1", - "xmlhttprequest-ssl": "~1.5.4", - "yeast": "0.1.2" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "engine.io-parser": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", - "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", - "dev": true, - "requires": { - "after": "0.8.2", - "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.5", - "blob": "0.0.5", - "has-binary2": "~1.0.2" - } - }, - "enhanced-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", - "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", - "tapable": "^1.0.0" - } - }, - "ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", - "dev": true - }, - "entities": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", - "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" - }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "dev": true, - "requires": { - "prr": "~1.0.1" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", - "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", - "dev": true, - "requires": { - "es-to-primitive": "^1.1.1", - "function-bind": "^1.1.1", - "has": "^1.0.1", - "is-callable": "^1.1.3", - "is-regex": "^1.0.4" - } - }, - "es-to-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", - "dev": true, - "requires": { - "is-callable": "^1.1.1", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.1" - } - }, - "es5-ext": { - "version": "0.10.46", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz", - "integrity": "sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw==", - "dev": true, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "1" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-set": "~0.1.5", - "es6-symbol": "~3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-symbol": "3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "es6-templates": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/es6-templates/-/es6-templates-0.2.3.tgz", - "integrity": "sha1-XLmsn7He1usSOTQrgdeSu7QHjuQ=", - "dev": true, - "requires": { - "recast": "~0.11.12", - "through": "~2.3.6" - } - }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.14", - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escope": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", - "dev": true, - "requires": { - "es6-map": "^0.1.3", - "es6-weak-map": "^2.0.1", - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-2.13.1.tgz", - "integrity": "sha1-5MyPoPAJ+4KaquI4VaKTYL4fbBE=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "concat-stream": "^1.4.6", - "debug": "^2.1.1", - "doctrine": "^1.2.2", - "es6-map": "^0.1.3", - "escope": "^3.6.0", - "espree": "^3.1.6", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "file-entry-cache": "^1.1.1", - "glob": "^7.0.3", - "globals": "^9.2.0", - "ignore": "^3.1.2", - "imurmurhash": "^0.1.4", - "inquirer": "^0.12.0", - "is-my-json-valid": "^2.10.0", - "is-resolvable": "^1.0.0", - "js-yaml": "^3.5.1", - "json-stable-stringify": "^1.0.0", - "levn": "^0.3.0", - "lodash": "^4.0.0", - "mkdirp": "^0.5.0", - "optionator": "^0.8.1", - "path-is-absolute": "^1.0.0", - "path-is-inside": "^1.0.1", - "pluralize": "^1.2.1", - "progress": "^1.1.8", - "require-uncached": "^1.0.2", - "shelljs": "^0.6.0", - "strip-json-comments": "~1.0.1", - "table": "^3.7.8", - "text-table": "~0.2.0", - "user-home": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "shelljs": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz", - "integrity": "sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg=", - "dev": true - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "espree": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", - "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", - "dev": true, - "requires": { - "acorn": "^5.5.0", - "acorn-jsx": "^3.0.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "dev": true, - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "eventemitter3": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", - "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==", - "dev": true - }, - "events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", - "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", - "dev": true - }, - "eventsource": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", - "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", - "dev": true, - "requires": { - "original": "^1.0.0" - } - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "^0.1.0" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "^2.1.0" - } - }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "dev": true, - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "dev": true - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fastparse": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", - "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=", - "dev": true - }, - "faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "figgy-pudding": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", - "dev": true - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "file-entry-cache": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.3.1.tgz", - "integrity": "sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g=", - "dev": true, - "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" - } - }, - "file-loader": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.2.0.tgz", - "integrity": "sha512-+xZnaK5R8kBJrHK0/6HRlrKNamvVS5rjyuju+rnyxRGuwUJwpAMsVzUl5dz6rK8brkzjV6JpcFNjp6NqV0g1OQ==", - "dev": true, - "requires": { - "loader-utils": "^1.2.3", - "schema-utils": "^2.0.0" - }, - "dependencies": { - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "schema-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", - "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, - "fileset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", - "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", - "dev": true, - "requires": { - "glob": "^7.0.3", - "minimatch": "^3.0.3" - } - }, - "fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "dev": true, - "requires": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^3.0.0", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" - } - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - } - }, - "find-cache-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.0.0.tgz", - "integrity": "sha512-t7ulV1fmbxh5G9l/492O1p5+EBbr3uwpt6odhFTMc+nWyhmbloe+ja9BZ8pIBtqFWhOmCWVjx+pTW4zDkFoclw==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.0", - "pkg-dir": "^4.1.0" - }, - "dependencies": { - "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "find-index": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz", - "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=", - "dev": true - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } - } - }, - "flat-cache": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", - "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", - "dev": true, - "requires": { - "circular-json": "^0.3.1", - "graceful-fs": "^4.1.2", - "rimraf": "~2.6.2", - "write": "^0.2.1" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", - "dev": true - }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "follow-redirects": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.9.0.tgz", - "integrity": "sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A==", - "dev": true, - "requires": { - "debug": "^3.0.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "dev": true - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "front-matter": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-2.1.2.tgz", - "integrity": "sha1-91mDufL0E75ljJPf172M5AePXNs=", - "dev": true, - "requires": { - "js-yaml": "^3.4.6" - } - }, - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs-minipass": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.0.0.tgz", - "integrity": "sha512-40Qz+LFXmd9tzYVnnBmZvFfvAADfUA14TXPK1s7IfElJTIZ97rA8w4Kin7Wt5JBrC3ShnnFJO/5vPjPEeJIq9A==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "^2.1.0" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.1", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.1.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.5.1", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "dev": true, - "requires": { - "globule": "^1.0.0" - } - }, - "generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "dev": true, - "requires": { - "is-property": "^1.0.2" - } - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "requires": { - "is-property": "^1.0.0" - } - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "dev": true, - "requires": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true, - "requires": { - "is-glob": "^2.0.0" - } - }, - "glob2base": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", - "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", - "dev": true, - "requires": { - "find-index": "^0.1.1" - } - }, - "global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dev": true, - "requires": { - "global-prefix": "^3.0.0" - }, - "dependencies": { - "global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dev": true, - "requires": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "globule": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", - "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", - "dev": true, - "requires": { - "glob": "~7.1.1", - "lodash": "~4.17.10", - "minimatch": "~3.0.2" - } - }, - "gonzales-pe-sl": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/gonzales-pe-sl/-/gonzales-pe-sl-4.2.3.tgz", - "integrity": "sha1-aoaLw4BkXxQf7rBCxvl/zHG1n+Y=", - "dev": true, - "requires": { - "minimist": "1.1.x" - }, - "dependencies": { - "minimist": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz", - "integrity": "sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=", - "dev": true - } - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "graphiql": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-0.13.2.tgz", - "integrity": "sha512-4N2HmQQpUfApS1cxrTtoZ15tnR3EW88oUiqmza6GgNQYZZfDdBGphdQlBYsKcjAB/SnIOJort+RA1dB6kf4M7Q==", - "requires": { - "codemirror": "^5.47.0", - "codemirror-graphql": "^0.8.3", - "copy-to-clipboard": "^3.2.0", - "markdown-it": "^8.4.0" - } - }, - "graphql": { - "version": "14.4.2", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.4.2.tgz", - "integrity": "sha512-6uQadiRgnpnSS56hdZUSvFrVcQ6OF9y6wkxJfKquFtHlnl7+KSuWwSJsdwiK1vybm1HgcdbpGkCpvhvsVQ0UZQ==", - "requires": { - "iterall": "^1.2.2" - } - }, - "graphql-config": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-2.0.1.tgz", - "integrity": "sha512-eb4FzlODifHE/Q+91QptAmkGw39wL5ToinJ2556UUsGt2drPc4tzifL+HSnHSaxiIbH8EUhc/Fa6+neinF04qA==", - "requires": { - "graphql-import": "^0.4.4", - "graphql-request": "^1.5.0", - "js-yaml": "^3.10.0", - "lodash": "^4.17.4", - "minimatch": "^3.0.4" - } - }, - "graphql-import": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.4.5.tgz", - "integrity": "sha512-G/+I08Qp6/QGTb9qapknCm3yPHV0ZL7wbaalWFpxsfR8ZhZoTBe//LsbsCKlbALQpcMegchpJhpTSKiJjhaVqQ==", - "requires": { - "lodash": "^4.17.4" - } - }, - "graphql-language-service-interface": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-1.3.2.tgz", - "integrity": "sha512-sOxFV5sBSnYtKIFHtlmAHHVdhok7CRbvCPLcuHvL4Q1RSgKRsPpeHUDKU+yCbmlonOKn/RWEKaYWrUY0Sgv70A==", - "requires": { - "graphql-config": "2.0.1", - "graphql-language-service-parser": "^1.2.2", - "graphql-language-service-types": "^1.2.2", - "graphql-language-service-utils": "^1.2.2" - } - }, - "graphql-language-service-parser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.5.0.tgz", - "integrity": "sha512-DX3B6DfvKa28gJoywtnkkIUdZitWqKqBTrZ6CQV8V5wO3GzJalQKT0J+B56oDkS6MhjLt928Yu8fj63laNWfoA==", - "requires": { - "graphql-config": "2.2.1", - "graphql-language-service-types": "^1.5.0" - }, - "dependencies": { - "graphql-config": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-2.2.1.tgz", - "integrity": "sha512-U8+1IAhw9m6WkZRRcyj8ZarK96R6lQBQ0an4lp76Ps9FyhOXENC5YQOxOFGm5CxPrX2rD0g3Je4zG5xdNJjwzQ==", - "requires": { - "graphql-import": "^0.7.1", - "graphql-request": "^1.5.0", - "js-yaml": "^3.10.0", - "lodash": "^4.17.4", - "minimatch": "^3.0.4" - } - }, - "graphql-import": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.7.1.tgz", - "integrity": "sha512-YpwpaPjRUVlw2SN3OPljpWbVRWAhMAyfSba5U47qGMOSsPLi2gYeJtngGpymjm9nk57RFWEpjqwh4+dpYuFAPw==", - "requires": { - "lodash": "^4.17.4", - "resolve-from": "^4.0.0" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - } - } - }, - "graphql-language-service-types": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.5.0.tgz", - "integrity": "sha512-THxB15oPC56zlNVSwv7JCahuSUbI9xnUHdftjOqZOz5588qjlPw/UHWQ8V/k0/XwZvH/TwCkmnBkIRmPVb1S5Q==", - "requires": { - "graphql-config": "2.2.1" - }, - "dependencies": { - "graphql-config": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-2.2.1.tgz", - "integrity": "sha512-U8+1IAhw9m6WkZRRcyj8ZarK96R6lQBQ0an4lp76Ps9FyhOXENC5YQOxOFGm5CxPrX2rD0g3Je4zG5xdNJjwzQ==", - "requires": { - "graphql-import": "^0.7.1", - "graphql-request": "^1.5.0", - "js-yaml": "^3.10.0", - "lodash": "^4.17.4", - "minimatch": "^3.0.4" - } - }, - "graphql-import": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.7.1.tgz", - "integrity": "sha512-YpwpaPjRUVlw2SN3OPljpWbVRWAhMAyfSba5U47qGMOSsPLi2gYeJtngGpymjm9nk57RFWEpjqwh4+dpYuFAPw==", - "requires": { - "lodash": "^4.17.4", - "resolve-from": "^4.0.0" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - } - } - }, - "graphql-language-service-utils": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-1.2.2.tgz", - "integrity": "sha512-98hzn1Dg3sSAiB+TuvNwWAoBrzuHs8NylkTK26TFyBjozM5wBZttp+T08OvOt+9hCFYRa43yRPrWcrs78KH9Hw==", - "requires": { - "graphql-config": "2.0.1", - "graphql-language-service-types": "^1.2.2" - } - }, - "graphql-request": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-1.8.2.tgz", - "integrity": "sha512-dDX2M+VMsxXFCmUX0Vo0TopIZIX4ggzOtiCsThgtrKR4niiaagsGTDIHj3fsOMFETpa064vzovI+4YV4QnMbcg==", - "requires": { - "cross-fetch": "2.2.2" - } - }, - "handle-thing": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", - "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", - "dev": true - }, - "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", - "dev": true, - "requires": { - "neo-async": "^2.6.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - } - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-binary2": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", - "dev": true, - "requires": { - "isarray": "2.0.1" - }, - "dependencies": { - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - } - } - }, - "has-cors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, - "hex-color-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", - "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", - "dev": true - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", - "dev": true - }, - "hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "hsl-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", - "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", - "dev": true - }, - "hsla-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", - "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", - "dev": true - }, - "html-comment-regex": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", - "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", - "dev": true - }, - "html-entities": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", - "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", - "dev": true - }, - "html-loader": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-0.5.5.tgz", - "integrity": "sha512-7hIW7YinOYUpo//kSYcPB6dCKoceKLmOwjEMmhIobHuWGDVl0Nwe4l68mdG/Ru0wcUxQjVMEoZpkalZ/SE7zog==", - "dev": true, - "requires": { - "es6-templates": "^0.2.3", - "fastparse": "^1.1.1", - "html-minifier": "^3.5.8", - "loader-utils": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "html-minifier": { - "version": "3.5.19", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.19.tgz", - "integrity": "sha512-Qr2JC9nsjK8oCrEmuB430ZIA8YWbF3D5LSjywD75FTuXmeqacwHgIM8wp3vHYzzPbklSjp53RdmDuzR4ub2HzA==", - "dev": true, - "requires": { - "camel-case": "3.0.x", - "clean-css": "4.1.x", - "commander": "2.16.x", - "he": "1.1.x", - "param-case": "2.1.x", - "relateurl": "0.2.x", - "uglify-js": "3.4.x" - } - }, - "html-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", - "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", - "dev": true, - "requires": { - "html-minifier": "^3.2.3", - "loader-utils": "^0.2.16", - "lodash": "^4.17.3", - "pretty-error": "^2.0.2", - "tapable": "^1.0.0", - "toposort": "^1.0.0", - "util.promisify": "1.0.0" - }, - "dependencies": { - "loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", - "dev": true, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0", - "object-assign": "^4.0.1" - } - } - } - }, - "htmlparser2": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", - "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", - "dev": true, - "requires": { - "domelementtype": "1", - "domhandler": "2.1", - "domutils": "1.1", - "readable-stream": "1.0" - }, - "dependencies": { - "domutils": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz", - "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", - "dev": true - }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "http-parser-js": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", - "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", - "dev": true - }, - "http-proxy": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", - "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", - "dev": true, - "requires": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-proxy-middleware": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", - "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", - "dev": true, - "requires": { - "http-proxy": "^1.17.0", - "is-glob": "^4.0.0", - "lodash": "^4.17.11", - "micromatch": "^3.1.10" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", - "dev": true - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "icss-utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", - "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", - "dev": true, - "requires": { - "postcss": "^7.0.14" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true - }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", - "dev": true - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "ignore-loader": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ignore-loader/-/ignore-loader-0.1.2.tgz", - "integrity": "sha1-2B8kA3bQuk8Nd4lyw60lh0EXpGM=", - "dev": true - }, - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - }, - "dependencies": { - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - }, - "dependencies": { - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - } - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "in-publish": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", - "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", - "dev": true - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", - "dev": true - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true - }, - "infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true - }, - "inquirer": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", - "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", - "dev": true, - "requires": { - "ansi-escapes": "^1.1.0", - "ansi-regex": "^2.0.0", - "chalk": "^1.0.0", - "cli-cursor": "^1.0.1", - "cli-width": "^2.0.0", - "figures": "^1.3.5", - "lodash": "^4.3.0", - "readline2": "^1.0.1", - "run-async": "^0.1.0", - "rx-lite": "^3.1.2", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "internal-ip": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", - "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", - "dev": true, - "requires": { - "default-gateway": "^4.2.0", - "ipaddr.js": "^1.9.0" - } - }, - "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", - "dev": true - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true - }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", - "dev": true - }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", - "dev": true - }, - "ipaddr.js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", - "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", - "dev": true - }, - "is-absolute-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", - "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-arguments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true - }, - "is-color-stop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", - "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", - "dev": true, - "requires": { - "css-color-names": "^0.0.4", - "hex-color-regex": "^1.1.0", - "hsl-regex": "^1.0.0", - "hsla-regex": "^1.0.0", - "rgb-regex": "^1.0.1", - "rgba-regex": "^1.0.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true - }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "dev": true, - "requires": { - "is-primitive": "^2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, - "is-my-ip-valid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", - "dev": true - }, - "is-my-json-valid": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz", - "integrity": "sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q==", - "dev": true, - "requires": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^4.0.0", - "xtend": "^4.0.0" - } - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true - }, - "is-path-in-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", - "dev": true, - "requires": { - "is-path-inside": "^2.1.0" - } - }, - "is-path-inside": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", - "dev": true, - "requires": { - "path-is-inside": "^1.0.2" - } - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-svg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", - "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", - "dev": true, - "requires": { - "html-comment-regex": "^1.1.0" - } - }, - "is-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isbinaryfile": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", - "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", - "dev": true, - "requires": { - "buffer-alloc": "^1.2.0" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul-api": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.6.tgz", - "integrity": "sha512-x0Eicp6KsShG1k1rMgBAi/1GgY7kFGEBwQpw3PXGEmu+rBcBNhqU8g2DgY9mlepAsLPzrzrbqSgCGANnki4POA==", - "dev": true, - "requires": { - "async": "^2.6.2", - "compare-versions": "^3.4.0", - "fileset": "^2.0.3", - "istanbul-lib-coverage": "^2.0.5", - "istanbul-lib-hook": "^2.0.7", - "istanbul-lib-instrument": "^3.3.0", - "istanbul-lib-report": "^2.0.8", - "istanbul-lib-source-maps": "^3.0.6", - "istanbul-reports": "^2.2.4", - "js-yaml": "^3.13.1", - "make-dir": "^2.1.0", - "minimatch": "^3.0.4", - "once": "^1.4.0" - }, - "dependencies": { - "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", - "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", - "dev": true, - "requires": { - "@babel/generator": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/template": "^7.4.0", - "@babel/traverse": "^7.4.3", - "@babel/types": "^7.4.0", - "istanbul-lib-coverage": "^2.0.5", - "semver": "^6.0.0" - } - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "istanbul-instrumenter-loader": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz", - "integrity": "sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w==", - "dev": true, - "requires": { - "convert-source-map": "^1.5.0", - "istanbul-lib-instrument": "^1.7.3", - "loader-utils": "^1.1.0", - "schema-utils": "^0.3.0" - }, - "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "schema-utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", - "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", - "dev": true, - "requires": { - "ajv": "^5.0.0" - } - } - } - }, - "istanbul-lib-coverage": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", - "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", - "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", - "dev": true, - "requires": { - "append-transform": "^1.0.0" - } - }, - "istanbul-lib-instrument": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", - "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", - "dev": true, - "requires": { - "babel-generator": "^6.18.0", - "babel-template": "^6.16.0", - "babel-traverse": "^6.18.0", - "babel-types": "^6.18.0", - "babylon": "^6.18.0", - "istanbul-lib-coverage": "^1.2.1", - "semver": "^5.3.0" - } - }, - "istanbul-lib-report": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", - "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "supports-color": "^6.1.0" - }, - "dependencies": { - "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", - "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "rimraf": "^2.6.3", - "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "istanbul-reports": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", - "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", - "dev": true, - "requires": { - "handlebars": "^4.1.2" - } - }, - "iterall": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.2.2.tgz", - "integrity": "sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==" - }, - "jasmine-core": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", - "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", - "dev": true - }, - "jest-worker": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", - "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", - "dev": true, - "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^6.1.0" - }, - "dependencies": { - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "js-base64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", - "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json3": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", - "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==", - "dev": true - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "karma": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/karma/-/karma-4.3.0.tgz", - "integrity": "sha512-NSPViHOt+RW38oJklvYxQC4BSQsv737oQlr/r06pCM+slDOr4myuI1ivkRmp+3dVpJDfZt2DmaPJ2wkx+ZZuMQ==", - "dev": true, - "requires": { - "bluebird": "^3.3.0", - "body-parser": "^1.16.1", - "braces": "^3.0.2", - "chokidar": "^3.0.0", - "colors": "^1.1.0", - "connect": "^3.6.0", - "core-js": "^3.1.3", - "di": "^0.0.1", - "dom-serialize": "^2.2.0", - "flatted": "^2.0.0", - "glob": "^7.1.1", - "graceful-fs": "^4.1.2", - "http-proxy": "^1.13.0", - "isbinaryfile": "^3.0.0", - "lodash": "^4.17.14", - "log4js": "^4.0.0", - "mime": "^2.3.1", - "minimatch": "^3.0.2", - "optimist": "^0.6.1", - "qjobs": "^1.1.4", - "range-parser": "^1.2.0", - "rimraf": "^2.6.0", - "safe-buffer": "^5.0.1", - "socket.io": "2.1.1", - "source-map": "^0.6.1", - "tmp": "0.0.33", - "useragent": "2.3.0" - }, - "dependencies": { - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "binary-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chokidar": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.2.1.tgz", - "integrity": "sha512-/j5PPkb5Feyps9e+jo07jUZGvkB5Aj953NrI4s8xSVScrAo/RHeILrtdb4uzR7N6aaFFxxJ+gt8mA8HfNpw76w==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.0", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.1.3" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fsevents": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.0.tgz", - "integrity": "sha512-+iXhW3LuDQsno8dOIrCIT/CBjeBWuP7PXe8w9shnj9Lebny/Gx1ZjVBYwexLz36Ri2jKuXMNpV6CYNh8lHHgrQ==", - "dev": true, - "optional": true - }, - "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "readdirp": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.1.3.tgz", - "integrity": "sha512-ZOsfTGkjO2kqeR5Mzr5RYDbTGYneSkdNKX2fOX2P5jF7vMrd/GNnIAUtDldeHHumHUCQ3V05YfWUdxMPAsRu9Q==", - "dev": true, - "requires": { - "picomatch": "^2.0.4" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "karma-chrome-launcher": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz", - "integrity": "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==", - "dev": true, - "requires": { - "which": "^1.2.1" - } - }, - "karma-cli": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/karma-cli/-/karma-cli-2.0.0.tgz", - "integrity": "sha512-1Kb28UILg1ZsfqQmeELbPzuEb5C6GZJfVIk0qOr8LNYQuYWmAaqP16WpbpKEjhejDrDYyYOwwJXSZO6u7q5Pvw==", - "dev": true, - "requires": { - "resolve": "^1.3.3" - } - }, - "karma-coverage-istanbul-reporter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.1.0.tgz", - "integrity": "sha512-UH0mXPJFJyK5uiK7EkwGtQ8f30lCBAfqRResnZ4pzLJ04SOp4SPlYkmwbbZ6iVJ6sQFVzlDUXlntBEsLRdgZpg==", - "dev": true, - "requires": { - "istanbul-api": "^2.1.6", - "minimatch": "^3.0.4" - } - }, - "karma-htmlfile-reporter": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/karma-htmlfile-reporter/-/karma-htmlfile-reporter-0.3.8.tgz", - "integrity": "sha512-Hd4c/vqPXYjdNYXeDJRMMq2DMMxPxqOR+TPeiLz2qbqO0qCCQMeXwFGhNDFr+GsvYhcOyn7maTbWusUFchS/4A==", - "dev": true, - "requires": { - "xmlbuilder": "^10.0.0" - } - }, - "karma-jasmine": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-2.0.1.tgz", - "integrity": "sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA==", - "dev": true, - "requires": { - "jasmine-core": "^3.3" - } - }, - "karma-jasmine-html-reporter": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.4.2.tgz", - "integrity": "sha512-7g0gPj8+9JepCNJR9WjDyQ2RkZ375jpdurYQyAYv8PorUCadepl8vrD6LmMqOGcM17cnrynBawQYZHaumgDjBw==", - "dev": true - }, - "karma-mocha-reporter": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz", - "integrity": "sha1-FRIAlejtgZGG5HoLAS8810GJVWA=", - "dev": true, - "requires": { - "chalk": "^2.1.0", - "log-symbols": "^2.1.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "karma-sourcemap-loader": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz", - "integrity": "sha1-kTIsd/jxPUb+0GKwQuEAnUxFBdg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2" - } - }, - "karma-webpack": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-4.0.2.tgz", - "integrity": "sha512-970/okAsdUOmiMOCY8sb17A2I8neS25Ad9uhyK3GHgmRSIFJbDcNEFE8dqqUhNe9OHiCC9k3DMrSmtd/0ymP1A==", - "dev": true, - "requires": { - "clone-deep": "^4.0.1", - "loader-utils": "^1.1.0", - "neo-async": "^2.6.1", - "schema-utils": "^1.0.0", - "source-map": "^0.7.3", - "webpack-dev-middleware": "^3.7.0" - }, - "dependencies": { - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", - "dev": true - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "killable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", - "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - }, - "known-css-properties": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.3.0.tgz", - "integrity": "sha512-QMQcnKAiQccfQTqtBh/qwquGZ2XK/DXND1jrcN9M8gMMy99Gwla7GQjndVUsEqIaRyP6bsFRuhwRj5poafBGJQ==", - "dev": true - }, - "last-call-webpack-plugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz", - "integrity": "sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==", - "dev": true, - "requires": { - "lodash": "^4.17.5", - "webpack-sources": "^1.1.0" - } - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "requires": { - "invert-kv": "^1.0.0" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", - "requires": { - "uc.micro": "^1.0.1" - } - }, - "loader-runner": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", - "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", - "dev": true - }, - "loader-utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", - "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", - "dev": true, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" - }, - "lodash.capitalize": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", - "integrity": "sha1-+CbJtOKoUR2E46yinbBeGk87cqk=", - "dev": true - }, - "lodash.kebabcase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", - "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=", - "dev": true - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "requires": { - "chalk": "^2.0.1" - } - }, - "log4js": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.5.1.tgz", - "integrity": "sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw==", - "dev": true, - "requires": { - "date-format": "^2.0.0", - "debug": "^4.1.1", - "flatted": "^2.0.0", - "rfdc": "^1.1.4", - "streamroller": "^1.0.6" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "loglevel": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.4.tgz", - "integrity": "sha512-p0b6mOGKcGa+7nnmKbpzR6qloPbrgLcnio++E+14Vo/XffOGwZtRpUhr8dTH/x2oCMmEoIU0Zwm3ZauhvYD17g==", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lower-case": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", - "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", - "dev": true - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "magic-string": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.4.tgz", - "integrity": "sha512-oycWO9nEVAP2RVPbIoDoA4Y7LFIJ3xRYov93gAyJhZkET1tNuB0u7uWkZS2LpBWTJUWnmau/To8ECWRC+jKNfw==", - "dev": true, - "requires": { - "sourcemap-codec": "^1.4.4" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - } - } - }, - "mamacro": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", - "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", - "dev": true - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "markdown-it": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", - "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", - "requires": { - "argparse": "^1.0.7", - "entities": "~1.1.1", - "linkify-it": "^2.0.0", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - } - }, - "marked": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==" - }, - "math-random": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", - "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", - "dev": true - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", - "dev": true - }, - "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true - }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - } - } - }, - "merge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", - "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", - "dev": true - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "mersenne-twister": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz", - "integrity": "sha1-+RZhjuQ9cXnvz2Qb7EUx65Zwl4o=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "^2.0.0", - "array-unique": "^0.2.1", - "braces": "^1.8.2", - "expand-brackets": "^0.1.4", - "extglob": "^0.3.1", - "filename-regex": "^2.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.1", - "kind-of": "^3.0.2", - "normalize-path": "^2.0.1", - "object.omit": "^2.0.0", - "parse-glob": "^3.0.4", - "regex-cache": "^0.4.2" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - } - }, - "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", - "dev": true - }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", - "dev": true - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, - "requires": { - "mime-db": "1.40.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "mini-css-extract-plugin": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz", - "integrity": "sha512-MNpRGbNA52q6U92i0qbVpQNsgk7LExy41MdAlG84FeytfDOtRIf/mCHdEgG8rpTKOaNKiqUnZdlptF469hxqOw==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "normalize-url": "1.9.1", - "schema-utils": "^1.0.0", - "webpack-sources": "^1.1.0" - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "minipass": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.0.1.tgz", - "integrity": "sha512-2y5okJ4uBsjoD2vAbLKL9EUQPPkC0YMIp+2mZOXG3nBba++pdfJWRxx2Ewirc0pwAJYu4XtWg2EkVo1nRXuO/w==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - }, - "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-pipeline": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz", - "integrity": "sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "dev": true, - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "moment": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" - }, - "mousetrap": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.3.tgz", - "integrity": "sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA==" - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "multicast-dns": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", - "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", - "dev": true, - "requires": { - "dns-packet": "^1.3.1", - "thunky": "^1.0.2" - } - }, - "multicast-dns-service-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", - "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", - "dev": true - }, - "mute-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", - "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", - "dev": true - }, - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", - "dev": true - }, - "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", - "dev": true - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, - "ngx-color-picker": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-8.2.0.tgz", - "integrity": "sha512-rzR+cByjNG9M/UskU5vNoH7cUc6oM8STTDFKOZmnlX4ALOuM1+61CBjsNTGETWfo9a/h5mbGX02oh5/iNAa7vA==" - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "no-case": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", - "dev": true, - "requires": { - "lower-case": "^1.1.1" - } - }, - "node-fetch": { - "version": "2.1.2", - "resolved": "http://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", - "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=" - }, - "node-forge": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", - "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", - "dev": true - }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "dev": true, - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "dev": true - } - } - }, - "node-libs-browser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", - "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", - "dev": true, - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.1", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.11.0", - "vm-browserify": "^1.0.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "dev": true, - "requires": { - "inherits": "2.0.3" - } - } - } - }, - "node-releases": { - "version": "1.1.34", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.34.tgz", - "integrity": "sha512-fNn12JTEfniTuCqo0r9jXgl44+KxRH/huV7zM/KAGOKxDKrHr6EbT7SSs4B+DNxyBE2mks28AD+Jw6PkfY5uwA==", - "dev": true, - "requires": { - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "node-sass": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", - "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", - "dev": true, - "requires": { - "async-foreach": "^0.1.3", - "chalk": "^1.1.1", - "cross-spawn": "^3.0.0", - "gaze": "^1.0.0", - "get-stdin": "^4.0.1", - "glob": "^7.0.3", - "in-publish": "^2.0.0", - "lodash": "^4.17.11", - "meow": "^3.7.0", - "mkdirp": "^0.5.1", - "nan": "^2.13.2", - "node-gyp": "^3.8.0", - "npmlog": "^4.0.0", - "request": "^2.88.0", - "sass-graph": "^2.2.4", - "stdout-stream": "^1.4.0", - "true-case-path": "^1.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "cross-spawn": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", - "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - }, - "nan": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", - "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", - "dev": true - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "resolve": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz", - "integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - } - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "normalize-url": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", - "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", - "dev": true, - "requires": { - "object-assign": "^4.0.1", - "prepend-http": "^1.0.0", - "query-string": "^4.1.0", - "sort-keys": "^1.0.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "nth-check": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", - "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", - "dev": true, - "requires": { - "boolbase": "~1.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-component": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", - "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "object-is": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", - "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", - "dev": true - }, - "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "dev": true, - "requires": { - "for-own": "^0.1.4", - "is-extendable": "^0.1.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "object.values": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", - "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, - "obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true - }, - "oidc-client": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/oidc-client/-/oidc-client-1.9.1.tgz", - "integrity": "sha512-AP1BwqASKIYrCBMu9dmNy3OTbhfaiBpy+5hZRbG1dmE2HqpQCp2JiJUNnNGTh2P+cnfVOrC79CGIluD1VMgMzQ==", - "requires": { - "base64-js": "^1.3.0", - "core-js": "^2.6.4", - "crypto-js": "^3.1.9-1", - "uuid": "^3.3.2" - }, - "dependencies": { - "core-js": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", - "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" - } - } - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "opn": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", - "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", - "dev": true, - "requires": { - "is-wsl": "^1.1.0" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - } - } - }, - "optimize-css-assets-webpack-plugin": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz", - "integrity": "sha512-q9fbvCRS6EYtUKKSwI87qm2IxlyJK5b4dygW1rKUBT6mMDhdG5e5bZT63v6tnJR9F9FB/H5a0HTmtw+laUBxKA==", - "dev": true, - "requires": { - "cssnano": "^4.1.10", - "last-call-webpack-plugin": "^3.0.0" - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - }, - "dependencies": { - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - } - } - }, - "original": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", - "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", - "dev": true, - "requires": { - "url-parse": "^1.4.3" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", - "dev": true - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "dependencies": { - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - } - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, - "p-limit": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", - "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "p-retry": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", - "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", - "dev": true, - "requires": { - "retry": "^0.12.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "pako": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", - "dev": true - }, - "parallel-transform": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", - "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", - "dev": true, - "requires": { - "cyclist": "^1.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - } - }, - "param-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", - "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", - "dev": true, - "requires": { - "no-case": "^2.2.0" - } - }, - "parse-asn1": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", - "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", - "dev": true, - "requires": { - "asn1.js": "^4.0.0", - "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "dev": true, - "requires": { - "glob-base": "^0.3.0", - "is-dotfile": "^1.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", - "dev": true - }, - "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", - "optional": true - }, - "parseqs": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", - "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseuri": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - }, - "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", - "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "picomatch": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", - "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pikaday": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/pikaday/-/pikaday-1.8.0.tgz", - "integrity": "sha512-SgGxMYX0NHj9oQnMaSyAipr2gOrbB4Lfs/TJTb6H6hRHs39/5c5VZi73Q8hr53+vWjdn6HzkWcj8Vtl3c9ziaA==" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "pluralize": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", - "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", - "dev": true - }, - "portfinder": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.24.tgz", - "integrity": "sha512-ekRl7zD2qxYndYflwiryJwMioBI7LI7rVXg3EnLK3sjkouT5eOuhS3gS255XxBksa30VG8UPZYZCdgfGOfkSUg==", - "dev": true, - "requires": { - "async": "^1.5.2", - "debug": "^2.2.0", - "mkdirp": "0.5.x" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "postcss": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", - "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-calc": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.1.tgz", - "integrity": "sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ==", - "dev": true, - "requires": { - "css-unit-converter": "^1.1.1", - "postcss": "^7.0.5", - "postcss-selector-parser": "^5.0.0-rc.4", - "postcss-value-parser": "^3.3.1" - }, - "dependencies": { - "cssesc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", - "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", - "dev": true - }, - "postcss-selector-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", - "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", - "dev": true, - "requires": { - "cssesc": "^2.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-colormin": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", - "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "color": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-convert-values": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", - "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-discard-comments": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", - "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-discard-duplicates": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", - "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-discard-empty": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", - "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-discard-overridden": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", - "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-merge-longhand": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", - "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", - "dev": true, - "requires": { - "css-color-names": "0.0.4", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "stylehacks": "^4.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-merge-rules": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", - "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "cssnano-util-same-parent": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0", - "vendors": "^1.0.0" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", - "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", - "dev": true, - "requires": { - "dot-prop": "^4.1.1", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-minify-font-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", - "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-minify-gradients": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", - "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "is-color-stop": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-minify-params": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", - "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "browserslist": "^4.0.0", - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "uniqs": "^2.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-minify-selectors": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", - "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", - "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", - "dev": true, - "requires": { - "dot-prop": "^4.1.1", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-modules-extract-imports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", - "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", - "dev": true, - "requires": { - "postcss": "^7.0.5" - } - }, - "postcss-modules-local-by-default": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", - "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", - "dev": true, - "requires": { - "icss-utils": "^4.1.1", - "postcss": "^7.0.16", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.0" - } - }, - "postcss-modules-scope": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz", - "integrity": "sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A==", - "dev": true, - "requires": { - "postcss": "^7.0.6", - "postcss-selector-parser": "^6.0.0" - } - }, - "postcss-modules-values": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", - "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", - "dev": true, - "requires": { - "icss-utils": "^4.0.0", - "postcss": "^7.0.6" - } - }, - "postcss-normalize-charset": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", - "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-normalize-display-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", - "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-positions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", - "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-repeat-style": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", - "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-string": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", - "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", - "dev": true, - "requires": { - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-timing-functions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", - "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-unicode": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", - "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-url": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", - "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", - "dev": true, - "requires": { - "is-absolute-url": "^2.0.0", - "normalize-url": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "normalize-url": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", - "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", - "dev": true - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-whitespace": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", - "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-ordered-values": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", - "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-reduce-initial": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", - "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0" - } - }, - "postcss-reduce-transforms": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", - "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-selector-parser": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", - "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "dependencies": { - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - } - } - }, - "postcss-svgo": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", - "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", - "dev": true, - "requires": { - "is-svg": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "svgo": "^1.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-unique-selectors": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", - "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "postcss": "^7.0.0", - "uniqs": "^2.0.0" - } - }, - "postcss-value-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", - "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", - "dev": true - }, - "postinstall-build": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.1.tgz", - "integrity": "sha1-uRepB5smF42aJK9aXNjLSpkdEbk=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, - "pretty-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", - "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", - "dev": true, - "requires": { - "renderkid": "^2.0.1", - "utila": "~0.4" - } - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", - "dev": true - }, - "progressbar.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/progressbar.js/-/progressbar.js-1.0.1.tgz", - "integrity": "sha1-9/v8GVJA/guzL2972y5/9ADqcfk=", - "requires": { - "shifty": "^1.5.2" - } - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", - "dev": true - }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "proxy-addr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", - "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", - "dev": true, - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.0" - } - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", - "dev": true - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, - "qjobs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", - "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", - "dev": true - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true - }, - "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", - "dev": true, - "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", - "dev": true - }, - "querystringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", - "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", - "dev": true - }, - "randomatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.0.tgz", - "integrity": "sha512-KnGPVE0lo2WoXxIZ7cPR8YBpiol4gsSuOwDSg410oHh80ZMp5EiypNqL2K4Z77vJn6lB5rap7IkAmcUlalcnBQ==", - "dev": true, - "requires": { - "is-number": "^4.0.0", - "kind-of": "^6.0.0", - "math-random": "^1.0.1" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true - }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "dev": true, - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "raw-loader": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-3.1.0.tgz", - "integrity": "sha512-lzUVMuJ06HF4rYveaz9Tv0WRlUMxJ0Y1hgSkkgg+50iEdaI0TthyEDe08KIHb0XsF6rn8WYTqPCaGTZg3sX+qA==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "schema-utils": "^2.0.1" - }, - "dependencies": { - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "schema-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.4.1.tgz", - "integrity": "sha512-RqYLpkPZX5Oc3fw/kHHHyP56fg5Y+XBpIpV8nCg0znIALfq3OH+Ea9Hfeac9BAMwG5IICltiZ0vxFvJQONfA5w==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1" - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - } - } - }, - "react": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react/-/react-16.10.2.tgz", - "integrity": "sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" - } - }, - "react-dom": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.10.2.tgz", - "integrity": "sha512-kWGDcH3ItJK4+6Pl9DZB16BXYAZyrYQItU4OMy0jAkv5aNqc+mAKb4TpFtAteI6TJZu+9ZlNhaeNQSVQDHJzkw==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.16.2" - } - }, - "react-is": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz", - "integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "minimatch": "^3.0.2", - "readable-stream": "^2.0.2", - "set-immediate-shim": "^1.0.1" - } - }, - "readline2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", - "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "mute-stream": "0.0.5" - } - }, - "recast": { - "version": "0.11.23", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz", - "integrity": "sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=", - "dev": true, - "requires": { - "ast-types": "0.9.6", - "esprima": "~3.1.0", - "private": "~0.1.5", - "source-map": "~0.5.0" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, - "reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", - "dev": true - }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "dev": true, - "requires": { - "is-equal-shallow": "^0.1.3" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexp.prototype.flags": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz", - "integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==", - "dev": true, - "requires": { - "define-properties": "^1.1.2" - } - }, - "regexpu-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", - "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", - "dev": true, - "requires": { - "regenerate": "^1.2.1", - "regjsgen": "^0.2.0", - "regjsparser": "^0.1.4" - } - }, - "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", - "dev": true - }, - "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - } - }, - "relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", - "dev": true - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "renderkid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz", - "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=", - "dev": true, - "requires": { - "css-select": "^1.1.0", - "dom-converter": "~0.1", - "htmlparser2": "~3.3.0", - "strip-ansi": "^3.0.0", - "utila": "~0.3" - }, - "dependencies": { - "utila": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", - "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", - "dev": true - } - } - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "^1.0.0" - } - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" - } - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, - "resolve": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", - "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", - "dev": true, - "requires": { - "path-parse": "^1.0.5" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - }, - "dependencies": { - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - } - } - } - }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", - "dev": true - }, - "rfdc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz", - "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==", - "dev": true - }, - "rgb-regex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", - "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", - "dev": true - }, - "rgba-regex": { - "version": "1.0.0", - "resolved": "http://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", - "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", - "dev": true - }, - "rimraf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", - "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "run-async": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", - "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", - "dev": true, - "requires": { - "once": "^1.3.0" - } - }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", - "dev": true, - "requires": { - "aproba": "^1.1.1" - } - }, - "rx-lite": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", - "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", - "dev": true - }, - "rxjs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", - "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", - "requires": { - "tslib": "^1.9.0" - } - }, - "rxjs-tslint": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/rxjs-tslint/-/rxjs-tslint-0.1.7.tgz", - "integrity": "sha512-NnOfqutNfdT7VQnQm32JLYh2gDZjc0gdWZFtrxf/czNGkLKJ1nOO6jbKAFI09W0f9lCtv6P2ozxjbQH8TSPPFQ==", - "dev": true, - "requires": { - "chalk": "^2.4.0", - "optimist": "^0.6.1", - "tslint": "^5.9.1", - "tsutils": "^2.25.0", - "typescript": ">=2.8.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sass-graph": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", - "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", - "dev": true, - "requires": { - "glob": "^7.0.0", - "lodash": "^4.0.0", - "scss-tokenizer": "^0.2.3", - "yargs": "^7.0.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, - "requires": { - "lcid": "^1.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - }, - "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true - }, - "yargs": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", - "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", - "dev": true, - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^5.0.0" - } - }, - "yargs-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", - "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", - "dev": true, - "requires": { - "camelcase": "^3.0.0" - } - } - } - }, - "sass-lint": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sass-lint/-/sass-lint-1.13.1.tgz", - "integrity": "sha512-DSyah8/MyjzW2BWYmQWekYEKir44BpLqrCFsgs9iaWiVTcwZfwXHF586hh3D1n+/9ihUNMfd8iHAyb9KkGgs7Q==", - "dev": true, - "requires": { - "commander": "^2.8.1", - "eslint": "^2.7.0", - "front-matter": "2.1.2", - "fs-extra": "^3.0.1", - "glob": "^7.0.0", - "globule": "^1.0.0", - "gonzales-pe-sl": "^4.2.3", - "js-yaml": "^3.5.4", - "known-css-properties": "^0.3.0", - "lodash.capitalize": "^4.1.0", - "lodash.kebabcase": "^4.0.0", - "merge": "^1.2.0", - "path-is-absolute": "^1.0.0", - "util": "^0.10.3" - }, - "dependencies": { - "fs-extra": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", - "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^3.0.0", - "universalify": "^0.1.0" - } - }, - "jsonfile": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", - "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - } - } - }, - "sass-loader": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.0.tgz", - "integrity": "sha512-+qeMu563PN7rPdit2+n5uuYVR0SSVwm0JsOUsaJXzgYcClWSlmX0iHDnmeOobPkf5kUglVot3QS6SyLyaQoJ4w==", - "dev": true, - "requires": { - "clone-deep": "^4.0.1", - "loader-utils": "^1.2.3", - "neo-async": "^2.6.1", - "schema-utils": "^2.1.0", - "semver": "^6.3.0" - }, - "dependencies": { - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", - "dev": true - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "schema-utils": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.2.0.tgz", - "integrity": "sha512-5EwsCNhfFTZvUreQhx/4vVQpJ/lnCAkgoIHLhSpp4ZirE+4hzFvdJi0FMub6hxbFVBJYSpeVVmon+2e7uEGRrA==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - } - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "scss-tokenizer": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", - "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", - "dev": true, - "requires": { - "js-base64": "^2.1.8", - "source-map": "^0.4.2" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", - "dev": true - }, - "selfsigned": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", - "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", - "dev": true, - "requires": { - "node-forge": "0.9.0" - } - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - }, - "semver-dsl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", - "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", - "dev": true, - "requires": { - "semver": "^5.3.0" - } - }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - } - } - }, - "serialize-javascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.0.tgz", - "integrity": "sha512-a/mxFfU00QT88umAJQsNWOnUKckhNCqOl028N48e7wFmo2/EHpTo9Wso+iJJCMrQnmFvcjto5RJdAHEvVhcyUQ==", - "dev": true - }, - "serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", - "dev": true, - "requires": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "dependencies": { - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - } - } - }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "dev": true - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shell-quote": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", - "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", - "dev": true, - "requires": { - "array-filter": "~0.0.0", - "array-map": "~0.0.0", - "array-reduce": "~0.0.0", - "jsonify": "~0.0.0" - } - }, - "shifty": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/shifty/-/shifty-1.5.4.tgz", - "integrity": "sha1-1DYvyRTdKA3fblIr5AiyEgMgg0Y=" - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "dev": true, - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "dev": true - } - } - }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true - }, - "slugify": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.5.tgz", - "integrity": "sha512-5VCnH7aS13b0UqWOs7Ef3E5rkhFe8Od+cp7wybFv5mv/sYSRkucZlJX0bamAJky7b2TTtGvrJBWVdpdEicsSrA==" - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - } - }, - "socket.io": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz", - "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==", - "dev": true, - "requires": { - "debug": "~3.1.0", - "engine.io": "~3.2.0", - "has-binary2": "~1.0.2", - "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.1.1", - "socket.io-parser": "~3.2.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "socket.io-adapter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", - "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=", - "dev": true - }, - "socket.io-client": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", - "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", - "dev": true, - "requires": { - "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", - "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "engine.io-client": "~3.2.0", - "has-binary2": "~1.0.2", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "socket.io-parser": "~3.2.0", - "to-array": "0.1.4" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "socket.io-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", - "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", - "dev": true, - "requires": { - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "isarray": "2.0.1" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - } - } - }, - "sockjs": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", - "dev": true, - "requires": { - "faye-websocket": "^0.10.0", - "uuid": "^3.0.1" - } - }, - "sockjs-client": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.4.0.tgz", - "integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==", - "dev": true, - "requires": { - "debug": "^3.2.5", - "eventsource": "^1.0.7", - "faye-websocket": "~0.11.1", - "inherits": "^2.0.3", - "json3": "^3.3.2", - "url-parse": "^1.4.3" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", - "dev": true, - "requires": { - "is-plain-obj": "^1.0.0" - } - }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "sourcemap-codec": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz", - "integrity": "sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==", - "dev": true - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", - "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==", - "dev": true - }, - "spdy": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.1.tgz", - "integrity": "sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "ssri": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-7.0.1.tgz", - "integrity": "sha512-FfndBvkXL9AHyGLNzU3r9AvYIBBZ7gm+m+kd0p8cT3/v4OliMAyipZAhLVEv1Zi/k4QFq9CstRGVd9pW/zcHFQ==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1", - "minipass": "^3.0.0" - } - }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true - }, - "stdout-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", - "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - } - }, - "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "dev": true, - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", - "dev": true - }, - "streamroller": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.6.tgz", - "integrity": "sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg==", - "dev": true, - "requires": { - "async": "^2.6.2", - "date-format": "^2.0.0", - "debug": "^3.2.6", - "fs-extra": "^7.0.1", - "lodash": "^4.17.14" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1" - } - }, - "strip-json-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", - "dev": true - }, - "style-loader": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.0.0.tgz", - "integrity": "sha512-B0dOCFwv7/eY31a5PCieNwMgMhVGFe9w+rh7s/Bx8kfFkrth9zfTZquoYvdw8URgiqxObQKcpW51Ugz1HjfdZw==", - "dev": true, - "requires": { - "loader-utils": "^1.2.3", - "schema-utils": "^2.0.1" - }, - "dependencies": { - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "schema-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", - "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "stylehacks": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", - "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", - "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", - "dev": true, - "requires": { - "dot-prop": "^4.1.1", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "subarg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", - "dev": true, - "requires": { - "minimist": "^1.1.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "svgo": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.0.tgz", - "integrity": "sha512-MLfUA6O+qauLDbym+mMZgtXCGRfIxyQoeH6IKVcFslyODEe/ElJNwr0FohQ3xG4C6HK6bk3KYPPXwHVJk3V5NQ==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.33", - "csso": "^3.5.1", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" - }, - "dependencies": { - "css-select": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz", - "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^2.1.2", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", - "dev": true - }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, - "requires": { - "boolbase": "~1.0.0" - } - } - } - }, - "table": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", - "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", - "dev": true, - "requires": { - "ajv": "^4.7.0", - "ajv-keywords": "^1.0.0", - "chalk": "^1.1.1", - "lodash": "^4.0.0", - "slice-ansi": "0.0.4", - "string-width": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "dev": true, - "requires": { - "co": "^4.6.0", - "json-stable-stringify": "^1.0.1" - } - }, - "ajv-keywords": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", - "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "tapable": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz", - "integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==", - "dev": true - }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "dev": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.2", - "inherits": "2" - } - }, - "terser": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.3.4.tgz", - "integrity": "sha512-Kcrn3RiW8NtHBP0ssOAzwa2MsIRQ8lJWiBG/K7JgqPlomA3mtb2DEmp4/hrUA+Jujx+WZ02zqd7GYD+QRBB/2Q==", - "dev": true, - "requires": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - }, - "dependencies": { - "commander": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", - "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", - "dev": true - } - } - }, - "terser-webpack-plugin": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.1.2.tgz", - "integrity": "sha512-MF/C4KABwqYOfRDi87f7gG07GP7Wj/kyiX938UxIGIO6l5mkh8XJL7xtS0hX/CRdVQaZI7ThGUPZbznrCjsGpg==", - "dev": true, - "requires": { - "cacache": "^13.0.0", - "find-cache-dir": "^3.0.0", - "jest-worker": "^24.9.0", - "schema-utils": "^2.4.1", - "serialize-javascript": "^2.1.0", - "source-map": "^0.6.1", - "terser": "^4.3.4", - "webpack-sources": "^1.4.3" - }, - "dependencies": { - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "schema-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.4.1.tgz", - "integrity": "sha512-RqYLpkPZX5Oc3fw/kHHHyP56fg5Y+XBpIpV8nCg0znIALfq3OH+Ea9Hfeac9BAMwG5IICltiZ0vxFvJQONfA5w==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1" - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "thunky": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.3.tgz", - "integrity": "sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow==", - "dev": true - }, - "timers-browserify": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", - "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", - "dev": true, - "requires": { - "setimmediate": "^1.0.4" - } - }, - "timsort": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", - "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", - "dev": true - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", - "dev": true - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true - }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - } - } - }, - "toggle-selection": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "dev": true - }, - "toposort": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", - "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", - "dev": true - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "tree-kill": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz", - "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==", - "dev": true - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, - "true-case-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", - "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", - "dev": true, - "requires": { - "glob": "^7.1.2" - } - }, - "ts-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.2.0.tgz", - "integrity": "sha512-Da8h3fD+HiZ9GvZJydqzk3mTC9nuOKYlJcpuk+Zv6Y1DPaMvBL+56GRzZFypx2cWrZFMsQr869+Ua2slGoLxvQ==", - "dev": true, - "requires": { - "chalk": "^2.3.0", - "enhanced-resolve": "^4.0.0", - "loader-utils": "^1.0.2", - "micromatch": "^4.0.0", - "semver": "^6.0.0" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "tsconfig-paths": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.5.0.tgz", - "integrity": "sha512-JYbN2zK2mxsv+bDVJCvSTxmdrD4R1qkG908SsqqD8TWjPNbSOtko1mnpQFFJo5Rbbc2/oJgDU9Cpkg/ZD7wNYg==", - "dev": true, - "requires": { - "@types/json5": "^0.0.29", - "deepmerge": "^2.0.1", - "json5": "^1.0.1", - "minimist": "^1.2.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } - } - }, - "tsconfig-paths-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.2.0.tgz", - "integrity": "sha512-S/gOOPOkV8rIL4LurZ1vUdYCVgo15iX9ZMJ6wx6w2OgcpT/G4wMyHB6WM+xheSqGMrWKuxFul+aXpCju3wmj/g==", - "dev": true, - "requires": { - "chalk": "^2.3.0", - "enhanced-resolve": "^4.0.0", - "tsconfig-paths": "^3.4.0" - } - }, - "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" - }, - "tslint": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.0.tgz", - "integrity": "sha512-2vqIvkMHbnx8acMogAERQ/IuINOq6DFqgF8/VDvhEkBqQh/x6SP0Y+OHnKth9/ZcHQSroOZwUQSN18v8KKF0/g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^4.0.1", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.8.0", - "tsutils": "^2.29.0" - }, - "dependencies": { - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - } - } - }, - "tslint-immutable": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/tslint-immutable/-/tslint-immutable-5.4.0.tgz", - "integrity": "sha512-8lZG7hNYRFOJv/p/Wb8/1cgizWSRpn4W3GSNWUVye9WyeO/LRbxp88pzNO8Een3RCMbHa3o7oW2UWa+Sx6hCBA==", - "dev": true, - "requires": { - "tsutils": "^2.28.0 || ^3.0.0" - } - }, - "tslint-webpack-plugin": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslint-webpack-plugin/-/tslint-webpack-plugin-2.1.0.tgz", - "integrity": "sha512-subYgmwihOGftPZS59looqPWdbqMIvsoTy8MeQPeZ7bOdwZfR3AAnVG8/VzpSRly8l/xbPosrX2QKtJEZPt71A==", - "dev": true, - "requires": { - "chalk": "^2.1.0" - } - }, - "tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "typemoq": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", - "integrity": "sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==", - "dev": true, - "requires": { - "circular-json": "^0.3.1", - "lodash": "^4.17.4", - "postinstall-build": "^5.0.1" - }, - "dependencies": { - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - } - } - }, - "typescript": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", - "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", - "dev": true - }, - "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" - }, - "uglify-js": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.7.tgz", - "integrity": "sha512-J0M2i1mQA+ze3EdN9SBi751DNdAXmeFLfJrd/MDIkRc3G3Gbb9OPVSx7GIQvVwfWxQARcYV2DTxIkMyDAk3o9Q==", - "dev": true, - "requires": { - "commander": "~2.16.0", - "source-map": "~0.6.1" - } - }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", - "dev": true - }, - "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==", - "dev": true - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", - "dev": true - }, - "uniqs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", - "dev": true - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dev": true, - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true - }, - "unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", - "dev": true - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true - }, - "upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", - "dev": true - }, - "uri-js": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", - "integrity": "sha1-+QuFhQf4HepNz7s8TD2/orVX+qo=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } - } - }, - "url-parse": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", - "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", - "dev": true, - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0" - } - }, - "useragent": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", - "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", - "dev": true, - "requires": { - "lru-cache": "4.1.x", - "tmp": "0.0.x" - } - }, - "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", - "dev": true - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "v8-compile-cache": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", - "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", - "dev": true - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true - }, - "vendors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.3.tgz", - "integrity": "sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vm-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", - "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", - "dev": true - }, - "void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", - "dev": true - }, - "watchpack": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", - "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", - "dev": true, - "requires": { - "chokidar": "^2.0.2", - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - } - } - }, - "wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "requires": { - "minimalistic-assert": "^1.0.0" - } - }, - "webpack": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.0.tgz", - "integrity": "sha512-yNV98U4r7wX1VJAj5kyMsu36T8RPPQntcb5fJLOsMz/pt/WrKC0Vp1bAlqPLkA1LegSwQwf6P+kAbyhRKVQ72g==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/wasm-edit": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5", - "acorn": "^6.2.1", - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.1.0", - "eslint-scope": "^4.0.3", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.4.0", - "loader-utils": "^1.2.3", - "memory-fs": "^0.4.1", - "micromatch": "^3.1.10", - "mkdirp": "^0.5.1", - "neo-async": "^2.6.1", - "node-libs-browser": "^2.2.1", - "schema-utils": "^1.0.0", - "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.1", - "watchpack": "^1.6.0", - "webpack-sources": "^1.4.1" - }, - "dependencies": { - "acorn": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", - "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", - "dev": true - }, - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", - "dev": true - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "cacache": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", - "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", - "dev": true, - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", - "dev": true - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "serialize-javascript": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz", - "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==", - "dev": true - }, - "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true - }, - "terser-webpack-plugin": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz", - "integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==", - "dev": true, - "requires": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^1.7.0", - "source-map": "^0.6.1", - "terser": "^4.1.2", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, - "webpack-cli": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.9.tgz", - "integrity": "sha512-xwnSxWl8nZtBl/AFJCOn9pG7s5CYUYdZxmmukv+fAHLcBIHM36dImfpQg3WfShZXeArkWlf6QRw24Klcsv8a5A==", - "dev": true, - "requires": { - "chalk": "2.4.2", - "cross-spawn": "6.0.5", - "enhanced-resolve": "4.1.0", - "findup-sync": "3.0.0", - "global-modules": "2.0.0", - "import-local": "2.0.0", - "interpret": "1.2.0", - "loader-utils": "1.2.3", - "supports-color": "6.1.0", - "v8-compile-cache": "2.0.3", - "yargs": "13.2.4" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yargs": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.0" - } - } - } - }, - "webpack-dev-middleware": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.0.tgz", - "integrity": "sha512-qvDesR1QZRIAZHOE3iQ4CXLZZSQ1lAUsSpnQmlB1PBfoN/xdRjmge3Dok0W4IdaVLJOGJy3sGI4sZHwjRU0PCA==", - "dev": true, - "requires": { - "memory-fs": "^0.4.1", - "mime": "^2.4.2", - "range-parser": "^1.2.1", - "webpack-log": "^2.0.0" - }, - "dependencies": { - "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", - "dev": true - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true - }, - "webpack-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", - "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", - "dev": true, - "requires": { - "ansi-colors": "^3.0.0", - "uuid": "^3.3.2" - } - } - } - }, - "webpack-dev-server": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.8.2.tgz", - "integrity": "sha512-0xxogS7n5jHDQWy0WST0q6Ykp7UGj4YvWh+HVN71JoE7BwPxMZrwgraBvmdEMbDVMBzF0u+mEzn8TQzBm5NYJQ==", - "dev": true, - "requires": { - "ansi-html": "0.0.7", - "bonjour": "^3.5.0", - "chokidar": "^2.1.8", - "compression": "^1.7.4", - "connect-history-api-fallback": "^1.6.0", - "debug": "^4.1.1", - "del": "^4.1.1", - "express": "^4.17.1", - "html-entities": "^1.2.1", - "http-proxy-middleware": "0.19.1", - "import-local": "^2.0.0", - "internal-ip": "^4.3.0", - "ip": "^1.1.5", - "is-absolute-url": "^3.0.3", - "killable": "^1.0.1", - "loglevel": "^1.6.4", - "opn": "^5.5.0", - "p-retry": "^3.0.1", - "portfinder": "^1.0.24", - "schema-utils": "^1.0.0", - "selfsigned": "^1.10.7", - "semver": "^6.3.0", - "serve-index": "^1.9.1", - "sockjs": "0.3.19", - "sockjs-client": "1.4.0", - "spdy": "^4.0.1", - "strip-ansi": "^3.0.1", - "supports-color": "^6.1.0", - "url": "^0.11.0", - "webpack-dev-middleware": "^3.7.2", - "webpack-log": "^2.0.0", - "ws": "^6.2.1", - "yargs": "12.0.5" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-absolute-url": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", - "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", - "dev": true - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "webpack-dev-middleware": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz", - "integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==", - "dev": true, - "requires": { - "memory-fs": "^0.4.1", - "mime": "^2.4.4", - "mkdirp": "^0.5.1", - "range-parser": "^1.2.1", - "webpack-log": "^2.0.0" - } - }, - "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "webpack-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", - "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", - "dev": true, - "requires": { - "ansi-colors": "^3.0.0", - "uuid": "^3.3.2" - } - }, - "webpack-sources": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", - "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "websocket-driver": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", - "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", - "dev": true, - "requires": { - "http-parser-js": ">=0.4.0 <0.4.11", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - } - }, - "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", - "dev": true - }, - "whatwg-fetch": { - "version": "2.0.4", - "resolved": "http://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", - "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - }, - "worker-farm": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", - "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", - "dev": true, - "requires": { - "errno": "~0.1.7" - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "ws": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - } - }, - "xhr2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz", - "integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8=" - }, - "xmlbuilder": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", - "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", - "dev": true - }, - "xmlhttprequest-ssl": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", - "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", - "dev": true - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, - "yargs": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.1.0.tgz", - "integrity": "sha512-1UhJbXfzHiPqkfXNHYhiz79qM/kZqjTE8yGlEjZa85Q+3+OwcV6NRkV7XOV1W2Eom2bzILeUn55pQYffjVOLAg==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - } - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - } - } - }, - "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "yeast": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", - "dev": true - }, - "zone.js": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.10.2.tgz", - "integrity": "sha512-UAYfiuvxLN4oyuqhJwd21Uxb4CNawrq6fPS/05Su5L4G+1TN+HVDJMUHNMobVQDFJRir2cLAODXwluaOKB7HFg==" - } - } -} diff --git a/src/Squidex/package.json b/src/Squidex/package.json deleted file mode 100644 index 13b16acf7..000000000 --- a/src/Squidex/package.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "name": "squidex", - "version": "1.0.0", - "description": "Squidex Headless CMS", - "license": "MIT", - "repository": "https://github.com/SebastianStehle/Squidex", - "scripts": { - "copy": "cpx node_modules/oidc-client/dist/oidc-client.min.js wwwroot/scripts/", - "start": "npm run copy && webpack-dev-server --config app-config/webpack.config.js --inline --port 3000 --hot", - "test": "karma start", - "test:coverage": "karma start karma.coverage.conf.js", - "test:clean": "rimraf _test-output", - "tslint": "tslint -c tslint.json -p tsconfig.json app/**/*.ts", - "build": "npm run copy && node --max_old_space_size=4096 node_modules/webpack/bin/webpack.js --config app-config/webpack.config.js --env.production", - "build:clean": "rimraf wwwroot/build" - }, - "dependencies": { - "@angular/animations": "8.2.9", - "@angular/cdk": "8.2.3", - "@angular/common": "8.2.9", - "@angular/core": "8.2.9", - "@angular/forms": "8.2.9", - "@angular/http": "7.2.15", - "@angular/platform-browser": "8.2.9", - "@angular/platform-browser-dynamic": "8.2.9", - "@angular/platform-server": "8.2.9", - "@angular/router": "8.2.9", - "angular2-chartjs": "0.5.1", - "babel-polyfill": "6.26.0", - "bootstrap": "4.3.1", - "core-js": "3.2.1", - "graphiql": "0.13.2", - "graphql": "14.4.2", - "marked": "0.7.0", - "mersenne-twister": "1.1.0", - "moment": "2.24.0", - "mousetrap": "1.6.3", - "ngx-color-picker": "8.2.0", - "oidc-client": "1.9.1", - "pikaday": "1.8.0", - "progressbar.js": "1.0.1", - "react": "16.10.2", - "react-dom": "16.10.2", - "rxjs": "6.5.3", - "slugify": "1.3.5", - "tslib": "1.10.0", - "zone.js": "0.10.2" - }, - "devDependencies": { - "@angular-devkit/build-optimizer": "0.803.8", - "@angular/compiler": "8.2.9", - "@angular/compiler-cli": "8.2.9", - "@ngtools/webpack": "8.3.8", - "@types/core-js": "2.5.2", - "@types/jasmine": "3.4.2", - "@types/marked": "0.6.5", - "@types/mersenne-twister": "1.1.2", - "@types/mousetrap": "1.6", - "@types/node": "12.7.11", - "@types/react": "16.9.5", - "@types/react-dom": "16.9.1", - "@types/sortablejs": "1.7.2", - "browserslist": "4.7.0", - "caniuse-lite": "1.0.30000998", - "circular-dependency-plugin": "5.2.0", - "codelyzer": "5.1.2", - "cpx": "1.5.0", - "css-loader": "3.2.0", - "file-loader": "4.2.0", - "html-loader": "0.5.5", - "html-webpack-plugin": "3.2.0", - "ignore-loader": "0.1.2", - "istanbul-instrumenter-loader": "3.0.1", - "jasmine-core": "3.5.0", - "karma": "4.3.0", - "karma-chrome-launcher": "3.1.0", - "karma-cli": "2.0.0", - "karma-coverage-istanbul-reporter": "2.1.0", - "karma-htmlfile-reporter": "0.3.8", - "karma-jasmine": "2.0.1", - "karma-jasmine-html-reporter": "1.4.2", - "karma-mocha-reporter": "2.2.5", - "karma-sourcemap-loader": "0.3.7", - "karma-webpack": "4.0.2", - "mini-css-extract-plugin": "0.8.0", - "node-sass": "4.12.0", - "optimize-css-assets-webpack-plugin": "5.0.3", - "raw-loader": "3.1.0", - "rimraf": "3.0.0", - "rxjs-tslint": "0.1.7", - "sass-lint": "1.13.1", - "sass-loader": "8.0.0", - "style-loader": "1.0.0", - "terser-webpack-plugin": "2.1.2", - "ts-loader": "6.2.0", - "tsconfig-paths-webpack-plugin": "3.2.0", - "tslint": "5.20.0", - "tslint-immutable": "5.4.0", - "tslint-webpack-plugin": "2.1.0", - "typemoq": "2.1.0", - "typescript": "3.5.3", - "underscore": "1.9.1", - "webpack": "4.41.0", - "webpack-cli": "3.3.9", - "webpack-dev-server": "3.8.2" - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs deleted file mode 100644 index b96f6637d..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Model.Apps -{ - public class RoleTests - { - [Fact] - public void Should_be_default_role() - { - var role = new Role("Owner"); - - Assert.True(role.IsDefault); - } - - [Fact] - public void Should_not_be_default_role() - { - var role = new Role("Custom"); - - Assert.False(role.IsDefault); - } - - [Fact] - public void Should_add_common_permission() - { - var role = new Role("Name"); - - var result = role.ForApp("my-app").Permissions.ToIds(); - - Assert.Equal(new[] { "squidex.apps.my-app.common" }, result); - } - - [Fact] - public void Should_not_have_duplicate_permission() - { - var role = new Role("Name", "common", "common", "common"); - - var result = role.ForApp("my-app").Permissions.ToIds(); - - Assert.Single(result); - } - - [Fact] - public void Should_ForApp_permission() - { - var role = new Role("Name", "clients.read"); - - var result = role.ForApp("my-app").Permissions.ToIds(); - - Assert.Equal("squidex.apps.my-app.clients.read", result.ElementAt(1)); - } - - [Fact] - public void Should_check_for_name() - { - var role = new Role("Custom"); - - Assert.True(role.Equals("Custom")); - } - - [Fact] - public void Should_check_for_null_name() - { - var role = new Role("Custom"); - - Assert.False(role.Equals(null)); - Assert.False(role.Equals("Other")); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs deleted file mode 100644 index 2a38e6a10..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs +++ /dev/null @@ -1,162 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Infrastructure.Security; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Core.Model.Apps -{ - public class RolesTests - { - private readonly Roles roles_0; - private readonly string firstRole = "Role1"; - private readonly string role = "Role2"; - - public RolesTests() - { - roles_0 = Roles.Empty.Add(firstRole); - } - - [Fact] - public void Should_create_roles_without_defaults() - { - var roles = new Roles(Roles.Defaults.ToArray()); - - Assert.Equal(0, roles.CustomCount); - } - - [Fact] - public void Should_add_role() - { - var roles_1 = roles_0.Add(role); - - roles_1[role].Should().BeEquivalentTo(new Role(role, PermissionSet.Empty)); - } - - [Fact] - public void Should_throw_exception_if_add_role_with_same_name() - { - var roles_1 = roles_0.Add(role); - - Assert.Throws(() => roles_1.Add(role)); - } - - [Fact] - public void Should_do_nothing_if_role_to_add_is_default() - { - var roles_1 = roles_0.Add(Role.Developer); - - Assert.True(roles_1.CustomCount > 0); - } - - [Fact] - public void Should_update_role() - { - var roles_1 = roles_0.Update(firstRole, "P1", "P2"); - - roles_1[firstRole].Should().BeEquivalentTo(new Role(firstRole, new PermissionSet("P1", "P2"))); - } - - [Fact] - public void Should_return_same_roles_if_role_not_found() - { - var roles_1 = roles_0.Update(role, "P1", "P2"); - - Assert.Same(roles_0, roles_1); - } - - [Fact] - public void Should_remove_role() - { - var roles_1 = roles_0.Remove(firstRole); - - Assert.Equal(0, roles_1.CustomCount); - } - - [Fact] - public void Should_do_nothing_if_remove_role_not_found() - { - var roles_1 = roles_0.Remove(role); - - Assert.True(roles_1.CustomCount > 0); - } - - [Fact] - public void Should_get_custom_roles() - { - var names = roles_0.Custom.Select(x => x.Name).ToArray(); - - Assert.Equal(new[] { firstRole }, names); - } - - [Fact] - public void Should_get_all_roles() - { - var names = roles_0.All.Select(x => x.Name).ToArray(); - - Assert.Equal(new[] { firstRole, "Owner", "Reader", "Editor", "Developer" }, names); - } - - [Fact] - public void Should_check_for_custom_role() - { - Assert.True(roles_0.ContainsCustom(firstRole)); - } - - [Fact] - public void Should_check_for_non_custom_role() - { - Assert.False(roles_0.ContainsCustom(Role.Owner)); - } - - [Fact] - public void Should_check_for_default_role() - { - Assert.True(Roles.IsDefault(Role.Owner)); - } - - [Fact] - public void Should_check_for_non_default_role() - { - Assert.False(Roles.IsDefault(firstRole)); - } - - [InlineData("Developer")] - [InlineData("Editor")] - [InlineData("Owner")] - [InlineData("Reader")] - [Theory] - public void Should_get_default_roles(string name) - { - var found = roles_0.TryGet("app", name, out var role); - - Assert.True(found); - Assert.True(role.IsDefault); - Assert.True(roles_0.Contains(name)); - - foreach (var permission in role.Permissions) - { - Assert.StartsWith("squidex.apps.app.", permission.Id); - } - } - - [Fact] - public void Should_return_null_if_role_not_found() - { - var found = roles_0.TryGet("app", "custom", out var role); - - Assert.False(found); - Assert.Null(role); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs deleted file mode 100644 index e0c7307db..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FluentAssertions; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Contents.Json; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Model.Contents -{ - public class WorkflowJsonTests - { - [Fact] - public void Should_serialize_and_deserialize() - { - var workflow = Workflow.Default; - - var serialized = workflow.SerializeAndDeserialize(); - - serialized.Should().BeEquivalentTo(workflow); - } - - [Fact] - public void Should_verify_roles_mapping_in_workflow_transition() - { - var source = new JsonWorkflowTransition { Expression = "expression_1", Role = "role_1" }; - - var serialized = source.SerializeAndDeserialize(); - - var result = serialized.ToTransition(); - - Assert.Single(result.Roles); - Assert.Equal(source.Role, result.Roles[0]); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs deleted file mode 100644 index f0601b933..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs +++ /dev/null @@ -1,147 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure.Collections; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Model.Contents -{ - public class WorkflowTests - { - private readonly Workflow workflow = new Workflow( - Status.Draft, new Dictionary - { - [Status.Draft] = - new WorkflowStep( - new Dictionary - { - [Status.Archived] = new WorkflowTransition("ToArchivedExpr", ReadOnlyCollection.Create("ToArchivedRole" )), - [Status.Published] = new WorkflowTransition("ToPublishedExpr", ReadOnlyCollection.Create("ToPublishedRole" )) - }, - StatusColors.Draft), - [Status.Archived] = - new WorkflowStep(), - [Status.Published] = - new WorkflowStep() - }); - - [Fact] - public void Should_provide_default_workflow_if_none_found() - { - var result = Workflows.Empty.GetFirst(); - - Assert.Same(Workflow.Default, result); - } - - [Fact] - public void Should_provide_initial_state() - { - var (status, step) = workflow.GetInitialStep(); - - Assert.Equal(Status.Draft, status); - Assert.Equal(StatusColors.Draft, step.Color); - Assert.Same(workflow.Steps[Status.Draft], step); - } - - [Fact] - public void Should_provide_step() - { - var found = workflow.TryGetStep(Status.Draft, out var step); - - Assert.True(found); - Assert.Same(workflow.Steps[Status.Draft], step); - } - - [Fact] - public void Should_not_provide_unknown_step() - { - var found = workflow.TryGetStep(default, out var step); - - Assert.False(found); - Assert.Null(step); - } - - [Fact] - public void Should_provide_transition() - { - var found = workflow.TryGetTransition(Status.Draft, Status.Archived, out var transition); - - Assert.True(found); - Assert.Equal("ToArchivedExpr", transition.Expression); - Assert.Equal(new[] { "ToArchivedRole" }, transition.Roles); - } - - [Fact] - public void Should_provide_transition_to_initial_if_step_not_found() - { - var found = workflow.TryGetTransition(new Status("Other"), Status.Draft, out var transition); - - Assert.True(found); - Assert.Null(transition.Expression); - Assert.Null(transition.Roles); - } - - [Fact] - public void Should_not_provide_transition_from_unknown_step() - { - var found = workflow.TryGetTransition(new Status("Other"), Status.Archived, out var transition); - - Assert.False(found); - Assert.Null(transition); - } - - [Fact] - public void Should_not_provide_transition_to_unknown_step() - { - var found = workflow.TryGetTransition(Status.Draft, default, out var transition); - - Assert.False(found); - Assert.Null(transition); - } - - [Fact] - public void Should_provide_transitions() - { - var transitions = workflow.GetTransitions(Status.Draft).ToArray(); - - Assert.Equal(2, transitions.Length); - - var (status1, step1, transition1) = transitions[0]; - - Assert.Equal(Status.Archived, status1); - Assert.Equal("ToArchivedExpr", transition1.Expression); - - Assert.Equal(new[] { "ToArchivedRole" }, transition1.Roles); - Assert.Same(workflow.Steps[status1], step1); - - var (status2, step2, transition2) = transitions[1]; - - Assert.Equal(Status.Published, status2); - Assert.Equal("ToPublishedExpr", transition2.Expression); - Assert.Equal(new[] { "ToPublishedRole" }, transition2.Roles); - Assert.Same(workflow.Steps[status2], step2); - } - - [Fact] - public void Should_provide_transitions_to_initial_step_if_status_not_found() - { - var transitions = workflow.GetTransitions(new Status("Other")).ToArray(); - - Assert.Single(transitions); - - var (status1, step1, transition1) = transitions[0]; - - Assert.Equal(Status.Draft, status1); - Assert.Null(transition1.Expression); - Assert.Null(transition1.Roles); - Assert.Same(workflow.Steps[status1], step1); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs deleted file mode 100644 index f16cdf930..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Xunit; - -namespace Squidex.Domain.Apps.Core.Model -{ - public class PartitioningTests - { - [Fact] - public void Should_consider_null_as_valid_partitioning() - { - string partitioning = null; - - Assert.True(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_consider_invariant_as_valid_partitioning() - { - var partitioning = "invariant"; - - Assert.True(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_consider_language_as_valid_partitioning() - { - var partitioning = "language"; - - Assert.True(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_not_consider_empty_as_valid_partitioning() - { - var partitioning = string.Empty; - - Assert.False(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_not_consider_other_string_as_valid_partitioning() - { - var partitioning = "invalid"; - - Assert.False(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_provide_invariant_instance() - { - Assert.Equal("invariant", Partitioning.Invariant.Key); - Assert.Equal("invariant", Partitioning.Invariant.ToString()); - } - - [Fact] - public void Should_provide_language_instance() - { - Assert.Equal("language", Partitioning.Language.Key); - Assert.Equal("language", Partitioning.Language.ToString()); - } - - [Fact] - public void Should_make_correct_equal_comparisons() - { - var partitioning1_a = new Partitioning("partitioning1"); - var partitioning1_b = new Partitioning("partitioning1"); - - var partitioning2 = new Partitioning("partitioning2"); - - Assert.Equal(partitioning1_a, partitioning1_b); - Assert.Equal(partitioning1_a.GetHashCode(), partitioning1_b.GetHashCode()); - Assert.True(partitioning1_a.Equals((object)partitioning1_b)); - - Assert.NotEqual(partitioning1_a, partitioning2); - Assert.NotEqual(partitioning1_a.GetHashCode(), partitioning2.GetHashCode()); - Assert.False(partitioning1_a.Equals((object)partitioning2)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs deleted file mode 100644 index eeaf343b7..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs +++ /dev/null @@ -1,169 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Infrastructure.Migrations; -using Squidex.Infrastructure.Reflection; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Core.Model.Rules -{ - public class RuleTests - { - public static readonly List Triggers = - typeof(Rule).Assembly.GetTypes() - .Where(x => x.BaseType == typeof(RuleTrigger)) - .Select(Activator.CreateInstance) - .Select(x => new[] { x }) - .ToList(); - - private readonly Rule rule_0 = new Rule(new ContentChangedTriggerV2(), new TestAction1()); - - public sealed class OtherTrigger : RuleTrigger - { - public override T Accept(IRuleTriggerVisitor visitor) - { - throw new NotSupportedException(); - } - } - - public sealed class MigratedTrigger : RuleTrigger, IMigrated - { - public override T Accept(IRuleTriggerVisitor visitor) - { - throw new NotSupportedException(); - } - - public RuleTrigger Migrate() - { - return new OtherTrigger(); - } - } - - [TypeName(nameof(TestAction1))] - public sealed class TestAction1 : RuleAction - { - public string Property { get; set; } - } - - [TypeName(nameof(TestAction2))] - public sealed class TestAction2 : RuleAction - { - public string Property { get; set; } - } - - [Fact] - public void Should_create_with_trigger_and_action() - { - var ruleTrigger = new ContentChangedTriggerV2(); - var ruleAction = new TestAction1(); - - var newRule = new Rule(ruleTrigger, ruleAction); - - Assert.Equal(ruleTrigger, newRule.Trigger); - Assert.Equal(ruleAction, newRule.Action); - Assert.True(newRule.IsEnabled); - } - - [Fact] - public void Should_set_enabled_to_true_when_enabling() - { - var rule_1 = rule_0.Disable(); - var rule_2 = rule_1.Enable(); - var rule_3 = rule_2.Enable(); - - Assert.False(rule_1.IsEnabled); - Assert.True(rule_3.IsEnabled); - } - - [Fact] - public void Should_set_enabled_to_false_when_disabling() - { - var rule_1 = rule_0.Disable(); - var rule_2 = rule_1.Disable(); - - Assert.True(rule_0.IsEnabled); - Assert.False(rule_2.IsEnabled); - } - - [Fact] - public void Should_replace_name_when_renaming() - { - var rule_1 = rule_0.Rename("MyName"); - - Assert.Equal("MyName", rule_1.Name); - } - - [Fact] - public void Should_replace_trigger_when_updating() - { - var newTrigger = new ContentChangedTriggerV2(); - - var rule_1 = rule_0.Update(newTrigger); - - Assert.NotSame(newTrigger, rule_0.Trigger); - Assert.Same(newTrigger, rule_1.Trigger); - } - - [Fact] - public void Should_throw_exception_when_new_trigger_has_other_type() - { - Assert.Throws(() => rule_0.Update(new OtherTrigger())); - } - - [Fact] - public void Should_replace_action_when_updating() - { - var newAction = new TestAction1(); - - var rule_1 = rule_0.Update(newAction); - - Assert.NotSame(newAction, rule_0.Action); - Assert.Same(newAction, rule_1.Action); - } - - [Fact] - public void Should_throw_exception_when_new_action_has_other_type() - { - Assert.Throws(() => rule_0.Update(new TestAction2())); - } - - [Fact] - public void Should_serialize_and_deserialize() - { - var rule_1 = rule_0.Disable(); - - var serialized = rule_1.SerializeAndDeserialize(); - - serialized.Should().BeEquivalentTo(rule_1); - } - - [Fact] - public void Should_serialize_and_deserialize_and_migrate_trigger() - { - var rule_X = new Rule(new MigratedTrigger(), new TestAction1()); - - var serialized = rule_X.SerializeAndDeserialize(); - - Assert.IsType(serialized.Trigger); - } - - [Theory] - [MemberData(nameof(Triggers))] - public void Should_freeze_triggers(RuleTrigger trigger) - { - TestUtils.TestFreeze(trigger); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs deleted file mode 100644 index 439600188..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Schemas; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Core.Model.Schemas -{ - public class SchemaFieldTests - { - public static readonly List FieldProperties = - typeof(Schema).Assembly.GetTypes() - .Where(x => x.BaseType == typeof(FieldProperties)) - .Select(Activator.CreateInstance) - .Select(x => new[] { x }) - .ToList(); - - private readonly RootField field_0 = Fields.Number(1, "my-field", Partitioning.Invariant); - - [Fact] - public void Should_instantiate_field() - { - Assert.True(field_0.RawProperties.IsFrozen); - Assert.Equal("my-field", field_0.Name); - } - - [Fact] - public void Should_throw_exception_if_creating_field_with_invalid_name() - { - Assert.Throws(() => Fields.Number(1, string.Empty, Partitioning.Invariant)); - } - - [Fact] - public void Should_hide_field() - { - var field_1 = field_0.Hide(); - var field_2 = field_1.Hide(); - - Assert.False(field_0.IsHidden); - Assert.True(field_2.IsHidden); - } - - [Fact] - public void Should_show_field() - { - var field_1 = field_0.Hide(); - var field_2 = field_1.Show(); - var field_3 = field_2.Show(); - - Assert.True(field_1.IsHidden); - Assert.False(field_3.IsHidden); - } - - [Fact] - public void Should_disable_field() - { - var field_1 = field_0.Disable(); - var field_2 = field_1.Disable(); - - Assert.False(field_0.IsDisabled); - Assert.True(field_2.IsDisabled); - } - - [Fact] - public void Should_enable_field() - { - var field_1 = field_0.Disable(); - var field_2 = field_1.Enable(); - var field_3 = field_2.Enable(); - - Assert.True(field_1.IsDisabled); - Assert.False(field_3.IsDisabled); - } - - [Fact] - public void Should_lock_field() - { - var field_1 = field_0.Lock(); - - Assert.False(field_0.IsLocked); - Assert.True(field_1.IsLocked); - } - - [Fact] - public void Should_update_field() - { - var field_1 = field_0.Update(new NumberFieldProperties { Hints = "my-hints" }); - - Assert.Null(field_0.RawProperties.Hints); - Assert.True(field_1.RawProperties.IsFrozen); - Assert.Equal("my-hints", field_1.RawProperties.Hints); - } - - [Fact] - public void Should_throw_exception_if_updating_with_invalid_properties_type() - { - Assert.Throws(() => field_0.Update(new StringFieldProperties())); - } - - [Theory] - [MemberData(nameof(FieldProperties))] - public void Should_freeze_field_properties(FieldProperties action) - { - TestUtils.TestFreeze(action); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs deleted file mode 100644 index 0429afaab..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs +++ /dev/null @@ -1,148 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -#pragma warning disable xUnit2013 // Do not use equality check to check for collection size. - -namespace Squidex.Domain.Apps.Core.Operations.ConvertContent -{ - public class ContentConversionFlatTests - { - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); - - [Fact] - public void Should_return_original_when_no_language_preferences_defined() - { - var data = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 1)); - - Assert.Same(data, data.ToFlatLanguageModel(languagesConfig)); - } - - [Fact] - public void Should_return_flatten_value() - { - var data = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("de", 1) - .AddValue("en", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("de", JsonValue.Null) - .AddValue("en", 4)) - .AddField("field3", - new ContentFieldData() - .AddValue("en", 6)) - .AddField("field4", - new ContentFieldData() - .AddValue("it", 7)); - - var output = data.ToFlatten(); - - var expected = new Dictionary - { - { - "field1", - new ContentFieldData() - .AddValue("de", 1) - .AddValue("en", 2) - }, - { - "field2", - new ContentFieldData() - .AddValue("de", JsonValue.Null) - .AddValue("en", 4) - }, - { "field3", JsonValue.Create(6) }, - { "field4", JsonValue.Create(7) } - }; - - Assert.True(expected.EqualsDictionary(output)); - } - - [Fact] - public void Should_return_flat_list_when_single_languages_specified() - { - var data = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("de", 1) - .AddValue("en", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("de", JsonValue.Null) - .AddValue("en", 4)) - .AddField("field3", - new ContentFieldData() - .AddValue("en", 6)) - .AddField("field4", - new ContentFieldData() - .AddValue("it", 7)); - - var fallbackConfig = - LanguagesConfig.Build( - new LanguageConfig(Language.EN), - new LanguageConfig(Language.DE, false, Language.EN)); - - var output = (Dictionary)data.ToFlatLanguageModel(fallbackConfig, new List { Language.DE }); - - var expected = new Dictionary - { - { "field1", JsonValue.Create(1) }, - { "field2", JsonValue.Create(4) }, - { "field3", JsonValue.Create(6) } - }; - - Assert.True(expected.EqualsDictionary(output)); - } - - [Fact] - public void Should_return_flat_list_when_languages_specified() - { - var data = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("de", 1) - .AddValue("en", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("de", JsonValue.Null) - .AddValue("en", 4)) - .AddField("field3", - new ContentFieldData() - .AddValue("en", 6)) - .AddField("field4", - new ContentFieldData() - .AddValue("it", 7)); - - var output = (Dictionary)data.ToFlatLanguageModel(languagesConfig, new List { Language.DE, Language.EN }); - - var expected = new Dictionary - { - { "field1", JsonValue.Create(1) }, - { "field2", JsonValue.Create(4) }, - { "field3", JsonValue.Create(6) } - }; - - Assert.True(expected.EqualsDictionary(output)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs deleted file mode 100644 index f525b284c..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs +++ /dev/null @@ -1,198 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.EnrichContent; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -#pragma warning disable xUnit2004 // Do not use equality check to test for boolean conditions - -namespace Squidex.Domain.Apps.Core.Operations.EnrichContent -{ - public class ContentEnrichmentTests - { - private readonly Instant now = Instant.FromUtc(2017, 10, 12, 16, 30, 10); - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); - private readonly Schema schema; - - public ContentEnrichmentTests() - { - schema = - new Schema("my-schema") - .AddString(1, "my-string", Partitioning.Language, - new StringFieldProperties { DefaultValue = "en-string" }) - .AddNumber(2, "my-number", Partitioning.Invariant, - new NumberFieldProperties()) - .AddDateTime(3, "my-datetime", Partitioning.Invariant, - new DateTimeFieldProperties { DefaultValue = now }) - .AddBoolean(4, "my-boolean", Partitioning.Invariant, - new BooleanFieldProperties { DefaultValue = true }); - } - - [Fact] - private void Should_enrich_with_default_values() - { - var data = - new NamedContentData() - .AddField("my-string", - new ContentFieldData() - .AddValue("de", "de-string")) - .AddField("my-number", - new ContentFieldData() - .AddValue("iv", 456)); - - data.Enrich(schema, languagesConfig.ToResolver()); - - Assert.Equal(456, ((JsonScalar)data["my-number"]["iv"]).Value); - - Assert.Equal("de-string", data["my-string"]["de"].ToString()); - Assert.Equal("en-string", data["my-string"]["en"].ToString()); - - Assert.Equal(now.ToString(), data["my-datetime"]["iv"].ToString()); - - Assert.True(((JsonScalar)data["my-boolean"]["iv"]).Value); - } - - [Fact] - private void Should_also_enrich_with_default_values_when_string_is_empty() - { - var data = - new NamedContentData() - .AddField("my-string", - new ContentFieldData() - .AddValue("de", string.Empty)) - .AddField("my-number", - new ContentFieldData() - .AddValue("iv", 456)); - - data.Enrich(schema, languagesConfig.ToResolver()); - - Assert.Equal("en-string", data["my-string"]["de"].ToString()); - Assert.Equal("en-string", data["my-string"]["en"].ToString()); - } - - [Fact] - public void Should_get_default_value_from_assets_field() - { - var field = - Fields.Assets(1, "1", Partitioning.Invariant, - new AssetsFieldProperties()); - - Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_boolean_field() - { - var field = - Fields.Boolean(1, "1", Partitioning.Invariant, - new BooleanFieldProperties { DefaultValue = true }); - - Assert.Equal(JsonValue.True, DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_datetime_field() - { - var field = - Fields.DateTime(1, "1", Partitioning.Invariant, - new DateTimeFieldProperties { DefaultValue = FutureDays(15) }); - - Assert.Equal(JsonValue.Create(FutureDays(15).ToString()), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_datetime_field_when_set_to_today() - { - var field = - Fields.DateTime(1, "1", Partitioning.Invariant, - new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Today }); - - Assert.Equal(JsonValue.Create("2017-10-12T00:00:00Z"), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_datetime_field_when_set_to_now() - { - var field = - Fields.DateTime(1, "1", Partitioning.Invariant, - new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now }); - - Assert.Equal(JsonValue.Create("2017-10-12T16:30:10Z"), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_json_field() - { - var field = - Fields.Json(1, "1", Partitioning.Invariant, - new JsonFieldProperties()); - - Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_geolocation_field() - { - var field = - Fields.Geolocation(1, "1", Partitioning.Invariant, - new GeolocationFieldProperties()); - - Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_number_field() - { - var field = - Fields.Number(1, "1", Partitioning.Invariant, - new NumberFieldProperties { DefaultValue = 12 }); - - Assert.Equal(JsonValue.Create(12), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_references_field() - { - var field = - Fields.References(1, "1", Partitioning.Invariant, - new ReferencesFieldProperties()); - - Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_string_field() - { - var field = - Fields.String(1, "1", Partitioning.Invariant, - new StringFieldProperties { DefaultValue = "default" }); - - Assert.Equal(JsonValue.Create("default"), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_tags_field() - { - var field = - Fields.Tags(1, "1", Partitioning.Invariant, - new TagsFieldProperties()); - - Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - private Instant FutureDays(int days) - { - return now.WithoutMs().Plus(Duration.FromDays(days)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs deleted file mode 100644 index 56d0093df..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs +++ /dev/null @@ -1,607 +0,0 @@ - -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.EventSynchronization; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization -{ - public class SchemaSynchronizerTests - { - private readonly Func idGenerator; - private readonly IJsonSerializer jsonSerializer = TestUtils.DefaultSerializer; - private readonly NamedId stringId = NamedId.Of(13L, "my-value"); - private readonly NamedId nestedId = NamedId.Of(141L, "my-value"); - private readonly NamedId arrayId = NamedId.Of(14L, "11-array"); - private int fields = 50; - - public SchemaSynchronizerTests() - { - idGenerator = () => fields++; - } - - [Fact] - public void Should_create_events_if_schema_deleted() - { - var sourceSchema = - new Schema("source"); - - var targetSchema = - (Schema)null; - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaDeleted() - ); - } - - [Fact] - public void Should_create_events_if_category_changed() - { - var sourceSchema = - new Schema("source"); - - var targetSchema = - new Schema("target") - .ChangeCategory("Category"); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaCategoryChanged { Name = "Category" } - ); - } - - [Fact] - public void Should_create_events_if_scripts_configured() - { - var scripts = new SchemaScripts - { - Create = "" - }; - - var sourceSchema = - new Schema("source"); - - var targetSchema = - new Schema("target").ConfigureScripts(scripts); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaScriptsConfigured { Scripts = scripts } - ); - } - - [Fact] - public void Should_create_events_if_preview_urls_configured() - { - var previewUrls = new Dictionary - { - ["web"] = "Url" - }; - - var sourceSchema = - new Schema("source"); - - var targetSchema = - new Schema("target") - .ConfigurePreviewUrls(previewUrls); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaPreviewUrlsConfigured { PreviewUrls = previewUrls } - ); - } - - [Fact] - public void Should_create_events_if_schema_published() - { - var sourceSchema = - new Schema("source"); - - var targetSchema = - new Schema("target") - .Publish(); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaPublished() - ); - } - - [Fact] - public void Should_create_events_if_schema_unpublished() - { - var sourceSchema = - new Schema("source") - .Publish(); - - var targetSchema = - new Schema("target"); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaUnpublished() - ); - } - - [Fact] - public void Should_create_events_if_nested_field_deleted() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_deleted() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var targetSchema = - new Schema("target"); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = stringId } - ); - } - - [Fact] - public void Should_create_events_if_nested_field_updated() - { - var properties = new StringFieldProperties { IsRequired = true }; - - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name, properties)); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldUpdated { Properties = properties, FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_updated() - { - var properties = new StringFieldProperties { IsRequired = true }; - - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant, properties); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldUpdated { Properties = properties, FieldId = stringId } - ); - } - - [Fact] - public void Should_create_events_if_nested_field_locked() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .LockField(nestedId.Id, arrayId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldLocked { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_locked() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .LockField(stringId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldLocked { FieldId = stringId } - ); - } - - [Fact] - public void Should_create_events_if_nested_field_hidden() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .HideField(nestedId.Id, arrayId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldHidden { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_hidden() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .HideField(stringId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldHidden { FieldId = stringId } - ); - } - - [Fact] - public void Should_create_events_if_nested_field_shown() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .HideField(nestedId.Id, arrayId.Id); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldShown { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_shown() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .HideField(stringId.Id); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldShown { FieldId = stringId } - ); - } - - [Fact] - public void Should_create_events_if_nested_field_disabled() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .DisableField(nestedId.Id, arrayId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldDisabled { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_disabled() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .DisableField(stringId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldDisabled { FieldId = stringId } - ); - } - - [Fact] - public void Should_create_events_if_nested_field_enabled() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .DisableField(nestedId.Id, arrayId.Id); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldEnabled { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_enabled() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .DisableField(stringId.Id); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldEnabled { FieldId = stringId } - ); - } - - [Fact] - public void Should_create_events_if_field_created() - { - var sourceSchema = - new Schema("source"); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .HideField(stringId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - var createdId = NamedId.Of(50L, stringId.Name); - - events.ShouldHaveSameEvents( - new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, - new FieldHidden { FieldId = createdId } - ); - } - - [Fact] - public void Should_create_events_if_field_type_has_changed() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddTags(stringId.Id, stringId.Name, Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - var createdId = NamedId.Of(50L, stringId.Name); - - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = stringId }, - new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new TagsFieldProperties() } - ); - } - - [Fact] - public void Should_create_events_if_field_partitioning_has_changed() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Language); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - var createdId = NamedId.Of(50L, stringId.Name); - - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = stringId }, - new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Language.Key, Properties = new StringFieldProperties() } - ); - } - - [Fact] - public void Should_create_events_if_nested_field_created() - { - var sourceSchema = - new Schema("source"); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .HideField(nestedId.Id, arrayId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - var id1 = NamedId.Of(50L, arrayId.Name); - var id2 = NamedId.Of(51L, stringId.Name); - - events.ShouldHaveSameEvents( - new FieldAdded { FieldId = id1, Name = arrayId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new ArrayFieldProperties() }, - new FieldAdded { FieldId = id2, Name = stringId.Name, ParentFieldId = id1, Properties = new StringFieldProperties() }, - new FieldHidden { FieldId = id2, ParentFieldId = id1 } - ); - } - - [Fact] - public void Should_create_events_if_nested_fields_reordered() - { - var id1 = NamedId.Of(1, "f1"); - var id2 = NamedId.Of(2, "f1"); - - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(10, "f1") - .AddString(11, "f2")); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(1, "f2") - .AddString(2, "f1")); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaFieldsReordered { FieldIds = new List { 11, 10 }, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_fields_reordered() - { - var id1 = NamedId.Of(1, "f1"); - var id2 = NamedId.Of(2, "f1"); - - var sourceSchema = - new Schema("source") - .AddString(10, "f1", Partitioning.Invariant) - .AddString(11, "f2", Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(1, "f2", Partitioning.Invariant) - .AddString(2, "f1", Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaFieldsReordered { FieldIds = new List { 11, 10 } } - ); - } - - [Fact] - public void Should_create_events_if_fields_reordered_after_sync() - { - var id1 = NamedId.Of(1, "f1"); - var id2 = NamedId.Of(2, "f1"); - - var sourceSchema = - new Schema("source") - .AddString(10, "f1", Partitioning.Invariant) - .AddString(11, "f2", Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(1, "f3", Partitioning.Invariant) - .AddString(2, "f1", Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = NamedId.Of(11L, "f2") }, - new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, - new SchemaFieldsReordered { FieldIds = new List { 50, 10 } } - ); - } - - [Fact] - public void Should_create_events_if_fields_reordered_after_sync2() - { - var id1 = NamedId.Of(1, "f1"); - var id2 = NamedId.Of(2, "f1"); - - var sourceSchema = - new Schema("source") - .AddString(10, "f1", Partitioning.Invariant) - .AddString(11, "f2", Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(1, "f1", Partitioning.Invariant) - .AddString(2, "f3", Partitioning.Invariant) - .AddString(3, "f2", Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, - new SchemaFieldsReordered { FieldIds = new List { 10, 50, 11 } } - ); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs deleted file mode 100644 index 7573a5194..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs +++ /dev/null @@ -1,308 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Core.ExtractReferenceIds; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -#pragma warning disable xUnit2013 // Do not use equality check to check for collection size. - -namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds -{ - public class ReferenceExtractionTests - { - private readonly Guid schemaId = Guid.NewGuid(); - private readonly Schema schema; - - public ReferenceExtractionTests() - { - schema = - new Schema("my-schema") - .AddNumber(1, "field1", Partitioning.Language) - .AddNumber(2, "field2", Partitioning.Invariant) - .AddNumber(3, "field3", Partitioning.Invariant) - .AddAssets(5, "assets1", Partitioning.Invariant) - .AddAssets(6, "assets2", Partitioning.Invariant) - .AddArray(7, "array", Partitioning.Invariant, a => a - .AddAssets(71, "assets71")) - .AddJson(4, "json", Partitioning.Language) - .UpdateField(3, f => f.Hide()); - } - - [Fact] - public void Should_get_ids_from_id_data() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var input = - new IdContentData() - .AddField(5, - new ContentFieldData() - .AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString()))); - - var ids = input.GetReferencedIds(schema).ToArray(); - - Assert.Equal(new[] { id1, id2 }, ids); - } - - [Fact] - public void Should_get_ids_from_name_data() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var input = - new NamedContentData() - .AddField("assets1", - new ContentFieldData() - .AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString()))); - - var ids = input.GetReferencedIds(schema).ToArray(); - - Assert.Equal(new[] { id1, id2 }, ids); - } - - [Fact] - public void Should_cleanup_deleted_ids() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var input = - new IdContentData() - .AddField(5, - new ContentFieldData() - .AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString()))); - - var converter = FieldConverters.ForValues(ValueReferencesConverter.CleanReferences(new[] { id2 })); - - var actual = input.ConvertId2Id(schema, converter); - - var cleanedValue = (JsonArray)actual[5]["iv"]; - - Assert.Equal(1, cleanedValue.Count); - Assert.Equal(id1.ToString(), cleanedValue[0].ToString()); - } - - [Fact] - public void Should_return_ids_from_assets_field() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); - - var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray(); - - Assert.Equal(new[] { id1, id2 }, result); - } - - [Fact] - public void Should_return_empty_list_from_assets_field_for_referenced_ids_when_null() - { - var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); - - var result = sut.GetReferencedIds(null).ToArray(); - - Assert.Empty(result); - } - - [Fact] - public void Should_return_empty_list_from_assets_field_for_referenced_ids_when_other_type() - { - var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); - - var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray(); - - Assert.Empty(result); - } - - [Fact] - public void Should_return_empty_list_from_non_references_field() - { - var sut = Fields.String(1, "my-string", Partitioning.Invariant); - - var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray(); - - Assert.Empty(result); - } - - [Fact] - public void Should_return_null_from_assets_field_when_removing_references_from_null_array() - { - var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); - - var result = sut.CleanReferences(JsonValue.Null, null); - - Assert.Equal(JsonValue.Null, result); - } - - [Fact] - public void Should_remove_deleted_references_from_assets_field() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); - - var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(id2)); - - Assert.Equal(CreateValue(id1), result); - } - - [Fact] - public void Should_return_same_token_from_assets_field_when_removing_references_and_nothing_to_remove() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); - - var token = CreateValue(id1, id2); - - var result = sut.CleanReferences(token, HashSet.Of(Guid.NewGuid())); - - Assert.Same(token, result); - } - - [Fact] - public void Should_return_ids_from_nested_references_field() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = - Fields.Array(1, "my-array", Partitioning.Invariant, - Fields.References(1, "my-refs", - new ReferencesFieldProperties { SchemaId = schemaId })); - - var value = - JsonValue.Array( - JsonValue.Object() - .Add("my-refs", CreateValue(id1, id2))); - - var result = sut.GetReferencedIds(value).ToArray(); - - Assert.Equal(new[] { id1, id2, schemaId }, result); - } - - [Fact] - public void Should_return_ids_from_references_field() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.References(1, "my-refs", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray(); - - Assert.Equal(new[] { id1, id2, schemaId }, result); - } - - [Fact] - public void Should_return_ids_from_references_field_without_schema_id() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.References(1, "my-refs", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.GetReferencedIds(CreateValue(id1, id2), Ids.ContentOnly).ToArray(); - - Assert.Equal(new[] { id1, id2 }, result); - } - - [Fact] - public void Should_return_list_from_references_field_with_schema_id_list_for_referenced_ids_when_null() - { - var sut = Fields.References(1, "my-refs", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.GetReferencedIds(JsonValue.Null).ToArray(); - - Assert.Equal(new[] { schemaId }, result); - } - - [Fact] - public void Should_return_list_from_references_field_with_schema_id_for_referenced_ids_when_other_type() - { - var sut = Fields.References(1, "my-refs", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray(); - - Assert.Equal(new[] { schemaId }, result); - } - - [Fact] - public void Should_return_null_from_references_field_when_removing_references_from_null_array() - { - var sut = Fields.References(1, "my-refs", Partitioning.Invariant); - - var result = sut.CleanReferences(JsonValue.Null, null); - - Assert.Equal(JsonValue.Null, result); - } - - [Fact] - public void Should_remove_deleted_references_from_references_field() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.References(1, "my-refs", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(id2)); - - Assert.Equal(CreateValue(id1), result); - } - - [Fact] - public void Should_remove_all_references_from_references_field_when_schema_is_removed() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.References(1, "my-refs", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(schemaId)); - - Assert.Equal(CreateValue(), result); - } - - [Fact] - public void Should_return_same_token_from_references_field_when_removing_references_and_nothing_to_remove() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.References(1, "my-refs", Partitioning.Invariant); - - var value = CreateValue(id1, id2); - - var result = sut.CleanReferences(value, HashSet.Of(Guid.NewGuid())); - - Assert.Same(value, result); - } - - private static IJsonValue CreateValue(params Guid[] ids) - { - return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs deleted file mode 100644 index 636c85caa..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ /dev/null @@ -1,331 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.Extensions.Options; -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Reflection; -using Xunit; - -#pragma warning disable xUnit2009 // Do not use boolean check to check for string equality - -namespace Squidex.Domain.Apps.Core.Operations.HandleRules -{ - public class RuleServiceTests - { - private readonly IRuleTriggerHandler ruleTriggerHandler = A.Fake(); - private readonly IRuleActionHandler ruleActionHandler = A.Fake(); - private readonly IEventEnricher eventEnricher = A.Fake(); - private readonly IClock clock = A.Fake(); - private readonly string actionData = "{\"value\":10}"; - private readonly string actionDump = "MyDump"; - private readonly string actionName = "ValidAction"; - private readonly string actionDescription = "MyDescription"; - private readonly Guid ruleId = Guid.NewGuid(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); - private readonly RuleService sut; - - public sealed class InvalidEvent : IEvent - { - } - - public sealed class InvalidAction : RuleAction - { - } - - public sealed class ValidAction : RuleAction - { - } - - public sealed class ValidData - { - public int Value { get; set; } - } - - public sealed class InvalidTrigger : RuleTrigger - { - public override T Accept(IRuleTriggerVisitor visitor) - { - return default; - } - } - - public RuleServiceTests() - { - typeNameRegistry.Map(typeof(ContentCreated)); - typeNameRegistry.Map(typeof(ValidAction), actionName); - - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); - - A.CallTo(() => ruleActionHandler.ActionType) - .Returns(typeof(ValidAction)); - - A.CallTo(() => ruleActionHandler.DataType) - .Returns(typeof(ValidData)); - - A.CallTo(() => ruleTriggerHandler.TriggerType) - .Returns(typeof(ContentChangedTriggerV2)); - - var log = A.Fake(); - - sut = new RuleService(Options.Create(new RuleOptions()), - new[] { ruleTriggerHandler }, - new[] { ruleActionHandler }, - eventEnricher, TestUtils.DefaultSerializer, clock, log, typeNameRegistry); - } - - [Fact] - public async Task Should_not_create_job_if_rule_disabled() - { - var @event = Envelope.Create(new ContentCreated()); - - var job = await sut.CreateJobAsync(ValidRule().Disable(), ruleId, @event); - - Assert.Null(job); - - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_create_job_for_invalid_event() - { - var @event = Envelope.Create(new InvalidEvent()); - - var job = await sut.CreateJobAsync(ValidRule(), ruleId, @event); - - Assert.Null(job); - - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_create_job_if_no_trigger_handler_registered() - { - var @event = Envelope.Create(new ContentCreated()); - - var job = await sut.CreateJobAsync(RuleInvalidTrigger(), ruleId, @event); - - Assert.Null(job); - - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_create_job_if_no_action_handler_registered() - { - var @event = Envelope.Create(new ContentCreated()); - - var job = await sut.CreateJobAsync(RuleInvalidAction(), ruleId, @event); - - Assert.Null(job); - - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_create_job_if_too_old() - { - var @event = Envelope.Create(new ContentCreated()).SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); - - var job = await sut.CreateJobAsync(ValidRule(), ruleId, @event); - - Assert.Null(job); - - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_create_job_if_not_triggered_with_precheck() - { - var rule = ValidRule(); - - var @event = Envelope.Create(new ContentCreated()); - - A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) - .Returns(false); - - var job = await sut.CreateJobAsync(rule, ruleId, @event); - - Assert.Null(job); - - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_create_job_if_enriched_event_not_created() - { - var rule = ValidRule(); - - var @event = Envelope.Create(new ContentCreated()); - - A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) - .Returns(true); - - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) - .Returns(Task.FromResult(null)); - - var job = await sut.CreateJobAsync(rule, ruleId, @event); - - Assert.Null(job); - } - - [Fact] - public async Task Should_not_create_job_if_not_triggered() - { - var rule = ValidRule(); - - var enrichedEvent = new EnrichedContentEvent { AppId = appId }; - - var @event = Envelope.Create(new ContentCreated()); - - A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) - .Returns(true); - - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) - .Returns(enrichedEvent); - - A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger)) - .Returns(false); - - var job = await sut.CreateJobAsync(rule, ruleId, @event); - - Assert.Null(job); - } - - [Fact] - public async Task Should_create_job_if_triggered() - { - var now = clock.GetCurrentInstant(); - - var rule = ValidRule(); - - var enrichedEvent = new EnrichedContentEvent { AppId = appId }; - - var @event = Envelope.Create(new ContentCreated()).SetTimestamp(now); - - A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) - .Returns(true); - - A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger)) - .Returns(true); - - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) - .Returns(enrichedEvent); - - A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, rule.Action)) - .Returns((actionDescription, new ValidData { Value = 10 })); - - var job = await sut.CreateJobAsync(rule, ruleId, @event); - - Assert.Equal(actionData, job.ActionData); - Assert.Equal(actionName, job.ActionName); - Assert.Equal(actionDescription, job.Description); - - Assert.Equal(now, job.Created); - Assert.Equal(now.Plus(Duration.FromDays(30)), job.Expires); - - Assert.Equal(enrichedEvent.AppId.Id, job.AppId); - - Assert.NotEqual(Guid.Empty, job.Id); - - A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent, A>.That.Matches(x => x.Payload == @event.Payload))) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_succeeded_job_with_full_dump_when_handler_returns_no_exception() - { - A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) - .Returns(Result.Success(actionDump)); - - var result = await sut.InvokeAsync(actionName, actionData); - - Assert.Equal(RuleResult.Success, result.Result.Status); - - Assert.True(result.Elapsed >= TimeSpan.Zero); - Assert.True(result.Result.Dump.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public async Task Should_return_failed_job_with_full_dump_when_handler_returns_exception() - { - A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) - .Returns(Result.Failed(new InvalidOperationException(), actionDump)); - - var result = await sut.InvokeAsync(actionName, actionData); - - Assert.Equal(RuleResult.Failed, result.Result.Status); - - Assert.True(result.Elapsed >= TimeSpan.Zero); - Assert.True(result.Result.Dump.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public async Task Should_return_timedout_job_with_full_dump_when_exception_from_handler_indicates_timeout() - { - A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) - .Returns(Result.Failed(new TimeoutException(), actionDump)); - - var result = await sut.InvokeAsync(actionName, actionData); - - Assert.Equal(RuleResult.Timeout, result.Result.Status); - - Assert.True(result.Elapsed >= TimeSpan.Zero); - Assert.True(result.Result.Dump.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); - - Assert.True(result.Result.Dump.IndexOf("Action timed out.", StringComparison.OrdinalIgnoreCase) >= 0); - } - - [Fact] - public async Task Should_create_exception_details_when_job_to_execute_failed() - { - var ex = new InvalidOperationException(); - - A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) - .Throws(ex); - - var result = await sut.InvokeAsync(actionName, actionData); - - Assert.Equal(ex, result.Result.Exception); - } - - private static Rule RuleInvalidAction() - { - return new Rule(new ContentChangedTriggerV2(), new InvalidAction()); - } - - private static Rule RuleInvalidTrigger() - { - return new Rule(new InvalidTrigger(), new ValidAction()); - } - - private static Rule ValidRule() - { - return new Rule(new ContentChangedTriggerV2(), new ValidAction()); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs deleted file mode 100644 index 6032337d1..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs +++ /dev/null @@ -1,134 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -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.Tags; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.Tags -{ - public class TagNormalizerTests - { - private readonly ITagService tagService = A.Fake(); - private readonly Guid appId = Guid.NewGuid(); - private readonly Guid schemaId = Guid.NewGuid(); - private readonly Schema schema; - - public TagNormalizerTests() - { - schema = - new Schema("my-schema") - .AddTags(1, "tags1", Partitioning.Invariant) - .AddTags(2, "tags2", Partitioning.Invariant, new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) - .AddString(3, "string", Partitioning.Invariant) - .AddArray(4, "array", Partitioning.Invariant, f => f - .AddTags(401, "nestedTags1") - .AddTags(402, "nestedTags2", new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) - .AddString(403, "string")); - } - - [Fact] - public async Task Should_normalize_tags_with_old_data() - { - var newData = GenerateData("n_raw"); - var oldData = GenerateData("o_raw"); - - A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), - A>.That.IsSameSequenceAs("n_raw2_1", "n_raw2_2", "n_raw4"), - A>.That.IsSameSequenceAs("o_raw2_1", "o_raw2_2", "o_raw4"))) - .Returns(new Dictionary - { - ["n_raw2_2"] = "id2_2", - ["n_raw2_1"] = "id2_1", - ["n_raw4"] = "id4" - }); - - await tagService.NormalizeAsync(appId, schemaId, schema, newData, oldData); - - Assert.Equal(JsonValue.Array("id2_1", "id2_2"), newData["tags2"]["iv"]); - Assert.Equal(JsonValue.Array("id4"), GetNestedTags(newData)); - } - - [Fact] - public async Task Should_normalize_tags_without_old_data() - { - var newData = GenerateData("name"); - - A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), - A>.That.IsSameSequenceAs("name2_1", "name2_2", "name4"), - A>.That.IsEmpty())) - .Returns(new Dictionary - { - ["name2_2"] = "id2_2", - ["name2_1"] = "id2_1", - ["name4"] = "id4" - }); - - await tagService.NormalizeAsync(appId, schemaId, schema, newData, null); - - Assert.Equal(JsonValue.Array("id2_1", "id2_2"), newData["tags2"]["iv"]); - Assert.Equal(JsonValue.Array("id4"), GetNestedTags(newData)); - } - - [Fact] - public async Task Should_denormalize_tags() - { - var newData = GenerateData("id"); - - A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), - A>.That.IsSameSequenceAs("id2_1", "id2_2", "id4"), - A>.That.IsEmpty())) - .Returns(new Dictionary - { - ["id2_2"] = "name2_2", - ["id2_1"] = "name2_1", - ["id4"] = "name4" - }); - - await tagService.NormalizeAsync(appId, schemaId, schema, newData, null); - - Assert.Equal(JsonValue.Array("name2_1", "name2_2"), newData["tags2"]["iv"]); - Assert.Equal(JsonValue.Array("name4"), GetNestedTags(newData)); - } - - private static IJsonValue GetNestedTags(NamedContentData newData) - { - var array = (JsonArray)newData["array"]["iv"]; - var arrayItem = (JsonObject)array[0]; - - return arrayItem["nestedTags2"]; - } - - private static NamedContentData GenerateData(string prefix) - { - return new NamedContentData() - .AddField("tags1", - new ContentFieldData() - .AddValue("iv", JsonValue.Array($"{prefix}1"))) - .AddField("tags2", - new ContentFieldData() - .AddValue("iv", JsonValue.Array($"{prefix}2_1", $"{prefix}2_2"))) - .AddField("string", - new ContentFieldData() - .AddValue("iv", $"{prefix}stringValue")) - .AddField("array", - new ContentFieldData() - .AddValue("iv", - JsonValue.Array( - JsonValue.Object() - .Add("nestedTags1", JsonValue.Array($"{prefix}3")) - .Add("nestedTags2", JsonValue.Array($"{prefix}4")) - .Add("string", $"{prefix}nestedStringValue")))); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs deleted file mode 100644 index d2df5f606..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs +++ /dev/null @@ -1,125 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public class ArrayFieldTests - { - private readonly List errors = new List(); - - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new ArrayFieldProperties()); - - Assert.Equal("my-array", sut.Name); - } - - [Fact] - public async Task Should_not_add_error_if_items_are_valid() - { - var sut = Field(new ArrayFieldProperties()); - - await sut.ValidateAsync(CreateValue(JsonValue.Object()), errors, ValidationTestExtensions.ValidContext); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_items_are_null_and_valid() - { - var sut = Field(new ArrayFieldProperties()); - - await sut.ValidateAsync(CreateValue(null), errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_number_of_items_is_equal_to_min_and_max_items() - { - var sut = Field(new ArrayFieldProperties { MinItems = 2, MaxItems = 2 }); - - await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_items_are_required_and_null() - { - var sut = Field(new ArrayFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(null), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_items_are_required_and_empty() - { - var sut = Field(new ArrayFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var sut = Field(new ArrayFieldProperties()); - - await sut.ValidateAsync(JsonValue.Create("invalid"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Not a valid value." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Field(new ArrayFieldProperties { MinItems = 3 }); - - await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Field(new ArrayFieldProperties { MaxItems = 1 }); - - await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } - - private static IJsonValue CreateValue(params JsonObject[] ids) - { - return ids == null ? JsonValue.Null : JsonValue.Array(ids.OfType().ToArray()); - } - - private static RootField Field(ArrayFieldProperties properties) - { - return Fields.Array(1, "my-array", Partitioning.Invariant, properties); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs deleted file mode 100644 index 5b1dfbe5b..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs +++ /dev/null @@ -1,321 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Infrastructure.Collections; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public class AssetsFieldTests - { - private readonly List errors = new List(); - - public sealed class AssetInfo : IAssetInfo - { - public Guid AssetId { get; set; } - - public string FileName { get; set; } - - public string FileHash { get; set; } - - public string Slug { get; set; } - - public long FileSize { get; set; } - - public bool IsImage { get; set; } - - public int? PixelWidth { get; set; } - - public int? PixelHeight { get; set; } - } - - private readonly AssetInfo document = new AssetInfo - { - AssetId = Guid.NewGuid(), - FileName = "MyDocument.pdf", - FileSize = 1024 * 4, - IsImage = false, - PixelWidth = null, - PixelHeight = null - }; - - private readonly AssetInfo image1 = new AssetInfo - { - AssetId = Guid.NewGuid(), - FileName = "MyImage.png", - FileSize = 1024 * 8, - IsImage = true, - PixelWidth = 800, - PixelHeight = 600 - }; - - private readonly AssetInfo image2 = new AssetInfo - { - AssetId = Guid.NewGuid(), - FileName = "MyImage.png", - FileSize = 1024 * 8, - IsImage = true, - PixelWidth = 800, - PixelHeight = 600 - }; - - private readonly ValidationContext ctx; - - public AssetsFieldTests() - { - ctx = ValidationTestExtensions.Assets(image1, image2, document); - } - - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new AssetsFieldProperties()); - - Assert.Equal("my-assets", sut.Name); - } - - [Fact] - public async Task Should_not_add_error_if_assets_are_valid() - { - var sut = Field(new AssetsFieldProperties()); - - await sut.ValidateAsync(CreateValue(document.AssetId), errors, ctx); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_assets_are_null_and_valid() - { - var sut = Field(new AssetsFieldProperties()); - - await sut.ValidateAsync(CreateValue(null), errors, ctx); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_number_of_assets_is_equal_to_min_and_max_items() - { - var sut = Field(new AssetsFieldProperties { MinItems = 2, MaxItems = 2 }); - - await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_duplicate_values_are_ignored() - { - var sut = Field(new AssetsFieldProperties { AllowDuplicates = true }); - - await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_assets_are_required_and_null() - { - var sut = Field(new AssetsFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(null), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_assets_are_required_and_empty() - { - var sut = Field(new AssetsFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var sut = Field(new AssetsFieldProperties()); - - await sut.ValidateAsync(JsonValue.Create("invalid"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Not a valid value." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Field(new AssetsFieldProperties { MinItems = 3 }); - - await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Field(new AssetsFieldProperties { MaxItems = 1 }); - - await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_asset_are_not_valid() - { - var assetId = Guid.NewGuid(); - - var sut = Field(new AssetsFieldProperties()); - - await sut.ValidateAsync(CreateValue(assetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { $"[1]: Id '{assetId}' not found." }); - } - - [Fact] - public async Task Should_add_error_if_document_is_too_small() - { - var sut = Field(new AssetsFieldProperties { MinSize = 5 * 1024 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[1]: \'4 kB\' less than minimum of \'5 kB\'." }); - } - - [Fact] - public async Task Should_add_error_if_document_is_too_big() - { - var sut = Field(new AssetsFieldProperties { MaxSize = 5 * 1024 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: \'8 kB\' greater than maximum of \'5 kB\'." }); - } - - [Fact] - public async Task Should_add_error_if_document_is_not_an_image() - { - var sut = Field(new AssetsFieldProperties { MustBeImage = true }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[1]: Not an image." }); - } - - [Fact] - public async Task Should_add_error_if_values_contains_duplicate() - { - var sut = Field(new AssetsFieldProperties { MustBeImage = true }); - - await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "Must not contain duplicate values." }); - } - - [Fact] - public async Task Should_add_error_if_image_width_is_too_small() - { - var sut = Field(new AssetsFieldProperties { MinWidth = 1000 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Width \'800px\' less than minimum of \'1000px\'." }); - } - - [Fact] - public async Task Should_add_error_if_image_width_is_too_big() - { - var sut = Field(new AssetsFieldProperties { MaxWidth = 700 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Width \'800px\' greater than maximum of \'700px\'." }); - } - - [Fact] - public async Task Should_add_error_if_image_height_is_too_small() - { - var sut = Field(new AssetsFieldProperties { MinHeight = 800 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Height \'600px\' less than minimum of \'800px\'." }); - } - - [Fact] - public async Task Should_add_error_if_image_height_is_too_big() - { - var sut = Field(new AssetsFieldProperties { MaxHeight = 500 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Height \'600px\' greater than maximum of \'500px\'." }); - } - - [Fact] - public async Task Should_add_error_if_image_has_invalid_aspect_ratio() - { - var sut = Field(new AssetsFieldProperties { AspectWidth = 1, AspectHeight = 1 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Aspect ratio not '1:1'." }); - } - - [Fact] - public async Task Should_add_error_if_image_has_invalid_extension() - { - var sut = Field(new AssetsFieldProperties { AllowedExtensions = ReadOnlyCollection.Create("mp4") }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] - { - "[1]: Invalid file extension.", - "[2]: Invalid file extension." - }); - } - - private static IJsonValue CreateValue(params Guid[] ids) - { - return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); - } - - private static RootField Field(AssetsFieldProperties properties) - { - return Fields.Assets(1, "my-assets", Partitioning.Invariant, properties); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs deleted file mode 100644 index e3d21769d..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs +++ /dev/null @@ -1,192 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public class ReferencesFieldTests - { - private readonly List errors = new List(); - private readonly Guid schemaId = Guid.NewGuid(); - private readonly Guid ref1 = Guid.NewGuid(); - private readonly Guid ref2 = Guid.NewGuid(); - - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new ReferencesFieldProperties()); - - Assert.Equal("my-refs", sut.Name); - } - - [Fact] - public async Task Should_not_add_error_if_references_are_valid() - { - var sut = Field(new ReferencesFieldProperties()); - - await sut.ValidateAsync(CreateValue(ref1), errors, Context()); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_references_are_null_and_valid() - { - var sut = Field(new ReferencesFieldProperties()); - - await sut.ValidateAsync(CreateValue(null), errors, Context()); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_number_of_references_is_equal_to_min_and_max_items() - { - var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2 }); - - await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_duplicate_values_are_allowed() - { - var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2, AllowDuplicates = true }); - - await sut.ValidateAsync(CreateValue(ref1, ref1), errors, Context()); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_schemas_not_defined() - { - var sut = Field(new ReferencesFieldProperties()); - - await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References((Guid.NewGuid(), ref1))); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_references_are_required_and_null() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); - - await sut.ValidateAsync(CreateValue(null), errors, Context()); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_references_are_required_and_empty() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); - - await sut.ValidateAsync(CreateValue(), errors, Context()); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var sut = Field(new ReferencesFieldProperties()); - - await sut.ValidateAsync(JsonValue.Create("invalid"), errors, Context()); - - errors.Should().BeEquivalentTo( - new[] { "Not a valid value." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MinItems = 3 }); - - await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); - - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MaxItems = 1 }); - - await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); - - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_reference_are_not_valid() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); - - await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References()); - - errors.Should().BeEquivalentTo( - new[] { $"Contains invalid reference '{ref1}'." }); - } - - [Fact] - public async Task Should_add_error_if_reference_schema_is_not_valid() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); - - await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References((Guid.NewGuid(), ref1))); - - errors.Should().BeEquivalentTo( - new[] { $"Contains reference '{ref1}' to invalid schema." }); - } - - [Fact] - public async Task Should_add_error_if_reference_contains_duplicate_values() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); - - await sut.ValidateAsync(CreateValue(ref1, ref1), errors, - ValidationTestExtensions.References( - (schemaId, ref1))); - - errors.Should().BeEquivalentTo( - new[] { "Must not contain duplicate values." }); - } - - private static IJsonValue CreateValue(params Guid[] ids) - { - return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); - } - - private ValidationContext Context() - { - return ValidationTestExtensions.References( - (schemaId, ref1), - (schemaId, ref2)); - } - - private static RootField Field(ReferencesFieldProperties properties) - { - return Fields.References(1, "my-refs", Partitioning.Invariant, properties); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs deleted file mode 100644 index db99fcede..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs +++ /dev/null @@ -1,139 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Collections; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public class StringFieldTests - { - private readonly List errors = new List(); - - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new StringFieldProperties()); - - Assert.Equal("my-string", sut.Name); - } - - [Fact] - public async Task Should_not_add_error_if_string_is_valid() - { - var sut = Field(new StringFieldProperties { Label = "" }); - - await sut.ValidateAsync(CreateValue(null), errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_string_is_required_but_null() - { - var sut = Field(new StringFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(null), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_string_is_required_but_empty() - { - var sut = Field(new StringFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(string.Empty), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_string_is_shorter_than_min_length() - { - var sut = Field(new StringFieldProperties { MinLength = 10 }); - - await sut.ValidateAsync(CreateValue("123"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must have at least 10 character(s)." }); - } - - [Fact] - public async Task Should_add_error_if_string_is_longer_than_max_length() - { - var sut = Field(new StringFieldProperties { MaxLength = 5 }); - - await sut.ValidateAsync(CreateValue("12345678"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 5 character(s)." }); - } - - [Fact] - public async Task Should_add_error_if_string_not_allowed() - { - var sut = Field(new StringFieldProperties { AllowedValues = ReadOnlyCollection.Create("Foo") }); - - await sut.ValidateAsync(CreateValue("Bar"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Not an allowed value." }); - } - - [Fact] - public async Task Should_add_error_if_number_is_not_valid_pattern() - { - var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}" }); - - await sut.ValidateAsync(CreateValue("abc"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Does not match to the pattern." }); - } - - [Fact] - public async Task Should_add_error_if_number_is_not_valid_pattern_with_message() - { - var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}", PatternMessage = "Custom Error Message." }); - - await sut.ValidateAsync(CreateValue("abc"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Custom Error Message." }); - } - - [Fact] - public async Task Should_add_error_if_unique_constraint_failed() - { - var sut = Field(new StringFieldProperties { IsUnique = true }); - - await sut.ValidateAsync(CreateValue("abc"), errors, ValidationTestExtensions.References((Guid.NewGuid(), Guid.NewGuid()))); - - errors.Should().BeEquivalentTo( - new[] { "Another content with the same value exists." }); - } - - private static IJsonValue CreateValue(string v) - { - return JsonValue.Create(v); - } - - private static RootField Field(StringFieldProperties properties) - { - return Fields.String(1, "my-string", Partitioning.Invariant, properties); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs deleted file mode 100644 index bab3782b2..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Collections; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public class TagsFieldTests - { - private readonly List errors = new List(); - - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new TagsFieldProperties()); - - Assert.Equal("my-tags", sut.Name); - } - - [Fact] - public async Task Should_not_add_error_if_tags_are_valid() - { - var sut = Field(new TagsFieldProperties()); - - await sut.ValidateAsync(CreateValue("tag"), errors, ValidationTestExtensions.ValidContext); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_tags_are_null_and_valid() - { - var sut = Field(new TagsFieldProperties()); - - await sut.ValidateAsync(CreateValue(null), errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_number_of_tags_is_equal_to_min_and_max_items() - { - var sut = Field(new TagsFieldProperties { MinItems = 2, MaxItems = 2 }); - - await sut.ValidateAsync(CreateValue("tag1", "tag2"), errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_tags_are_required_but_null() - { - var sut = Field(new TagsFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(null), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_tags_are_required_but_empty() - { - var sut = Field(new TagsFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_tag_value_is_null() - { - var sut = Field(new TagsFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(JsonValue.Array(JsonValue.Null), errors); - - errors.Should().BeEquivalentTo( - new[] { "[1]: Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_tag_value_is_empty() - { - var sut = Field(new TagsFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(string.Empty), errors); - - errors.Should().BeEquivalentTo( - new[] { "[1]: Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var sut = Field(new TagsFieldProperties()); - - await sut.ValidateAsync(JsonValue.Create("invalid"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Not a valid value." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Field(new TagsFieldProperties { MinItems = 3 }); - - await sut.ValidateAsync(CreateValue("tag-1", "tag-2"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Field(new TagsFieldProperties { MaxItems = 1 }); - - await sut.ValidateAsync(CreateValue("tag-1", "tag-2"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_value_contains_an_not_allowed_values() - { - var sut = Field(new TagsFieldProperties { AllowedValues = ReadOnlyCollection.Create("tag-2", "tag-3") }); - - await sut.ValidateAsync(CreateValue("tag-1", "tag-2", null), errors); - - errors.Should().BeEquivalentTo( - new[] { "[1]: Not an allowed value." }); - } - - private static IJsonValue CreateValue(params string[] ids) - { - return ids == null ? JsonValue.Null : JsonValue.Array(ids.OfType().ToArray()); - } - - private static RootField Field(TagsFieldProperties properties) - { - return Fields.Tags(1, "my-tags", Partitioning.Invariant, properties); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs deleted file mode 100644 index bb22186fe..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -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.ValidateContent; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public class UIFieldTests - { - private readonly List errors = new List(); - - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new UIFieldProperties()); - - Assert.Equal("my-ui", sut.Name); - } - - [Fact] - public async Task Should_not_add_error_if_value_is_undefined() - { - var sut = Field(new UIFieldProperties()); - - await sut.ValidateAsync(Undefined.Value, errors, ValidationTestExtensions.ValidContext); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_value_is_json_null() - { - var sut = Field(new UIFieldProperties()); - - await sut.ValidateAsync(JsonValue.Null, errors); - - errors.Should().BeEquivalentTo( - new[] { "Value must not be defined." }); - } - - [Fact] - public async Task Should_add_error_if_value_is_valid() - { - var sut = Field(new UIFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(JsonValue.True, errors); - - errors.Should().BeEquivalentTo( - new[] { "Value must not be defined." }); - } - - [Fact] - public async Task Should_add_error_if_field_object_is_defined() - { - var schema = - new Schema("my-schema") - .AddUI(1, "my-ui1", Partitioning.Invariant) - .AddUI(2, "my-ui2", Partitioning.Invariant); - - var data = - new NamedContentData() - .AddField("my-ui1", new ContentFieldData()) - .AddField("my-ui2", new ContentFieldData() - .AddValue("iv", null)); - - var validationContext = ValidationTestExtensions.ValidContext; - var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext); - - await validator.ValidateAsync(data); - - validator.Errors.Should().BeEquivalentTo( - new[] - { - new ValidationError("Value must not be defined.", "my-ui1"), - new ValidationError("Value must not be defined.", "my-ui2") - }); - } - - [Fact] - public async Task Should_add_error_if_array_item_field_is_defined() - { - var schema = - new Schema("my-schema") - .AddArray(1, "my-array", Partitioning.Invariant, array => array - .AddUI(101, "my-ui")); - - var data = - new NamedContentData() - .AddField("my-array", new ContentFieldData() - .AddValue("iv", - JsonValue.Array( - JsonValue.Object() - .Add("my-ui", null)))); - - var validationContext = - new ValidationContext( - Guid.NewGuid(), - Guid.NewGuid(), - (c, s) => null, - (s) => null, - (c) => null); - - var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext); - - await validator.ValidateAsync(data); - - validator.Errors.Should().BeEquivalentTo( - new[] { new ValidationError("Value must not be defined.", "my-array[1].my-ui") }); - } - - private static NestedField Field(UIFieldProperties properties) - { - return new NestedField(1, "my-ui", properties); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs deleted file mode 100644 index 66885c301..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Domain.Apps.Core.ValidateContent.Validators; - -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public static class ValidationTestExtensions - { - private static readonly Task> EmptyReferences = Task.FromResult>(new List<(Guid SchemaId, Guid Id)>()); - private static readonly Task> EmptyAssets = Task.FromResult>(new List()); - - public static readonly ValidationContext ValidContext = new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), - (x, y) => EmptyReferences, - (x) => EmptyReferences, - (x) => EmptyAssets); - - public static Task ValidateAsync(this IValidator validator, object value, IList errors, ValidationContext context = null) - { - return validator.ValidateAsync(value, - CreateContext(context), - CreateFormatter(errors)); - } - - public static Task ValidateOptionalAsync(this IValidator validator, object value, IList errors, ValidationContext context = null) - { - return validator.ValidateAsync( - value, - CreateContext(context).Optional(true), - CreateFormatter(errors)); - } - - public static Task ValidateAsync(this IField field, object value, IList errors, ValidationContext context = null) - { - return new FieldValidator(FieldValueValidatorsFactory.CreateValidators(field).ToArray(), field) - .ValidateAsync( - value, - CreateContext(context), - CreateFormatter(errors)); - } - - private static AddError CreateFormatter(IList errors) - { - return (field, message) => - { - if (field == null || !field.Any()) - { - errors.Add(message); - } - else - { - errors.Add($"{field.ToPathString()}: {message}"); - } - }; - } - - private static ValidationContext CreateContext(ValidationContext context) - { - return context ?? ValidContext; - } - - public static ValidationContext Assets(params IAssetInfo[] assets) - { - var actual = Task.FromResult>(assets.ToList()); - - return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => EmptyReferences, x => EmptyReferences, x => actual); - } - - public static ValidationContext References(params (Guid Id, Guid SchemaId)[] referencesIds) - { - var actual = Task.FromResult>(referencesIds.ToList()); - - return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => actual, x => actual, x => EmptyAssets); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj deleted file mode 100644 index ce93b49d2..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - Exe - netcoreapp2.2 - 2.2.0 - Squidex.Domain.Apps.Core - 7.3 - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - ..\..\Squidex.ruleset - - - - - diff --git a/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs b/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs deleted file mode 100644 index 2fb59c93e..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs +++ /dev/null @@ -1,173 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Squidex.Domain.Apps.Core.Apps.Json; -using Squidex.Domain.Apps.Core.Contents.Json; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules.Json; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Schemas.Json; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Newtonsoft; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.Reflection; -using Xunit; - -namespace Squidex.Domain.Apps.Core -{ - public static class TestUtils - { - public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); - - public static IJsonSerializer CreateSerializer(TypeNameHandling typeNameHandling = TypeNameHandling.Auto) - { - var typeNameRegistry = - new TypeNameRegistry() - .Map(new FieldRegistry()) - .Map(new RuleRegistry()) - .MapUnmapped(typeof(TestUtils).Assembly); - - var serializerSettings = new JsonSerializerSettings - { - SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry), - - ContractResolver = new ConverterContractResolver( - new AppClientsConverter(), - new AppContributorsConverter(), - new AppPatternsConverter(), - new ClaimsPrincipalConverter(), - new ContentFieldDataConverter(), - new EnvelopeHeadersConverter(), - new FilterConverter(), - new InstantConverter(), - new JsonValueConverter(), - new LanguageConverter(), - new LanguagesConfigConverter(), - new NamedGuidIdConverter(), - new NamedLongIdConverter(), - new NamedStringIdConverter(), - new PropertyPathConverter(), - new RefTokenConverter(), - new RolesConverter(), - new RuleConverter(), - new SchemaConverter(), - new StatusConverter(), - new StringEnumConverter(), - new WorkflowConverter(), - new WorkflowTransitionConverter()), - - TypeNameHandling = typeNameHandling - }; - - return new NewtonsoftJsonSerializer(serializerSettings); - } - - public static Schema MixedSchema(bool isSingleton = false) - { - var schema = new Schema("user", isSingleton: isSingleton) - .Publish() - .AddArray(101, "root-array", Partitioning.Language, f => f - .AddAssets(201, "nested-assets") - .AddBoolean(202, "nested-boolean") - .AddDateTime(203, "nested-datetime") - .AddGeolocation(204, "nested-geolocation") - .AddJson(205, "nested-json") - .AddJson(211, "nested-json2") - .AddNumber(206, "nested-number") - .AddReferences(207, "nested-references") - .AddString(208, "nested-string") - .AddTags(209, "nested-tags") - .AddUI(210, "nested-ui")) - .AddAssets(102, "root-assets", Partitioning.Invariant, - new AssetsFieldProperties()) - .AddBoolean(103, "root-boolean", Partitioning.Invariant, - new BooleanFieldProperties()) - .AddDateTime(104, "root-datetime", Partitioning.Invariant, - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime }) - .AddDateTime(105, "root-date", Partitioning.Invariant, - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date }) - .AddGeolocation(106, "root-geolocation", Partitioning.Invariant, - new GeolocationFieldProperties()) - .AddJson(107, "root-json", Partitioning.Invariant, - new JsonFieldProperties()) - .AddNumber(108, "root-number", Partitioning.Invariant, - new NumberFieldProperties { MinValue = 1, MaxValue = 10 }) - .AddReferences(109, "root-references", Partitioning.Invariant, - new ReferencesFieldProperties()) - .AddString(110, "root-string1", Partitioning.Invariant, - new StringFieldProperties { Label = "My String1", IsRequired = true, AllowedValues = ReadOnlyCollection.Create("a", "b") }) - .AddString(111, "root-string2", Partitioning.Invariant, - new StringFieldProperties { Hints = "My String1" }) - .AddTags(112, "root-tags", Partitioning.Language, - new TagsFieldProperties()) - .AddUI(113, "root-ui", Partitioning.Language, - new UIFieldProperties()) - .Update(new SchemaProperties { Hints = "The User" }) - .HideField(104) - .HideField(211, 101) - .DisableField(109) - .DisableField(212, 101) - .LockField(105); - - return schema; - } - - public static T SerializeAndDeserialize(this T value) - { - return DefaultSerializer.Deserialize(DefaultSerializer.Serialize(value)); - } - - public static void TestFreeze(IFreezable sut) - { - var properties = - sut.GetType().GetRuntimeProperties() - .Where(x => - x.CanWrite && - x.CanRead && - x.Name != "IsFrozen"); - - foreach (var property in properties) - { - var value = - property.PropertyType.IsValueType ? Activator.CreateInstance(property.PropertyType) : null; - - property.SetValue(sut, value); - - var result = property.GetValue(sut); - - Assert.Equal(value, result); - } - - sut.Freeze(); - - foreach (var property in properties) - { - var value = - property.PropertyType.IsValueType ? Activator.CreateInstance(property.PropertyType) : null; - - Assert.Throws(() => - { - try - { - property.SetValue(sut, value); - } - catch (Exception ex) - { - throw ex.InnerException; - } - }); - } - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs deleted file mode 100644 index 69087e8d7..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.State; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Validation; -using Xunit; - -#pragma warning disable IDE0067 // Dispose objects before losing scope - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public class AppCommandMiddlewareTests : HandlerTestBase - { - private readonly IContextProvider contextProvider = A.Fake(); - private readonly IAssetStore assetStore = A.Fake(); - private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); - private readonly Guid appId = Guid.NewGuid(); - private readonly Context requestContext = Context.Anonymous(); - private readonly AppCommandMiddleware sut; - - public sealed class MyCommand : SquidexCommand - { - } - - protected override Guid Id - { - get { return appId; } - } - - public AppCommandMiddlewareTests() - { - A.CallTo(() => contextProvider.Context) - .Returns(requestContext); - - sut = new AppCommandMiddleware(A.Fake(), assetStore, assetThumbnailGenerator, contextProvider); - } - - [Fact] - public async Task Should_replace_context_app_with_grain_result() - { - var result = A.Fake(); - - var command = CreateCommand(new MyCommand()); - var context = CreateContextForCommand(command); - - context.Complete(result); - - await sut.HandleAsync(context); - - Assert.Same(result, requestContext.App); - } - - [Fact] - public async Task Should_upload_image_to_store() - { - var stream = new MemoryStream(); - - var file = new AssetFile("name.jpg", "image/jpg", 1024, () => stream); - - var command = CreateCommand(new UploadAppImage { AppId = appId, File = file }); - var context = CreateContextForCommand(command); - - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) - .Returns(new ImageInfo(100, 100)); - - await sut.HandleAsync(context); - - A.CallTo(() => assetStore.UploadAsync(appId.ToString(), stream, true, A.Ignored)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_throw_exception_when_file_to_upload_is_not_an_image() - { - var stream = new MemoryStream(); - - var file = new AssetFile("name.jpg", "image/jpg", 1024, () => stream); - - var command = CreateCommand(new UploadAppImage { AppId = appId, File = file }); - var context = CreateContextForCommand(command); - - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) - .Returns(Task.FromResult(null)); - - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs deleted file mode 100644 index 36d8ef06a..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs +++ /dev/null @@ -1,658 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; -using Squidex.Domain.Apps.Entities.Apps.State; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Shared.Users; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public class AppGrainTests : HandlerTestBase - { - private readonly IAppPlansProvider appPlansProvider = A.Fake(); - private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); - private readonly IUser user = A.Fake(); - private readonly IUserResolver userResolver = A.Fake(); - private readonly string contributorId = Guid.NewGuid().ToString(); - private readonly string clientId = "client"; - private readonly string clientNewName = "My Client"; - private readonly string roleName = "My Role"; - private readonly string planIdPaid = "premium"; - private readonly string planIdFree = "free"; - private readonly AppGrain sut; - private readonly Guid workflowId = Guid.NewGuid(); - private readonly Guid patternId1 = Guid.NewGuid(); - private readonly Guid patternId2 = Guid.NewGuid(); - private readonly Guid patternId3 = Guid.NewGuid(); - private readonly InitialPatterns initialPatterns; - - protected override Guid Id - { - get { return AppId; } - } - - public AppGrainTests() - { - A.CallTo(() => user.Id) - .Returns(contributorId); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId)) - .Returns(user); - - A.CallTo(() => appPlansProvider.GetPlan(A.Ignored)) - .Returns(new ConfigAppLimitsPlan { MaxContributors = 10 }); - - initialPatterns = new InitialPatterns - { - { patternId1, new AppPattern("Number", "[0-9]") }, - { patternId2, new AppPattern("Numbers", "[0-9]*") } - }; - - sut = new AppGrain(initialPatterns, Store, A.Dummy(), appPlansProvider, appPlansBillingManager, userResolver); - sut.ActivateAsync(Id).Wait(); - } - - [Fact] - public async Task Command_should_throw_exception_if_app_is_archived() - { - await ExecuteCreateAsync(); - await ExecuteArchiveAsync(); - - await Assert.ThrowsAsync(ExecuteAttachClientAsync); - } - - [Fact] - public async Task Create_should_create_events_and_update_state() - { - var command = new CreateApp { Name = AppName, Actor = Actor, AppId = AppId }; - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(AppName, sut.Snapshot.Name); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppCreated { Name = AppName }), - CreateEvent(new AppContributorAssigned { ContributorId = Actor.Identifier, Role = Role.Owner }), - CreateEvent(new AppLanguageAdded { Language = Language.EN }), - CreateEvent(new AppPatternAdded { PatternId = patternId1, Name = "Number", Pattern = "[0-9]" }), - CreateEvent(new AppPatternAdded { PatternId = patternId2, Name = "Numbers", Pattern = "[0-9]*" }) - ); - } - - [Fact] - public async Task Update_should_create_events_and_update_state() - { - var command = new UpdateApp { Label = "my-label", Description = "my-description" }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal("my-label", sut.Snapshot.Label); - Assert.Equal("my-description", sut.Snapshot.Description); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppUpdated { Label = "my-label", Description = "my-description" }) - ); - } - - [Fact] - public async Task UploadImage_should_create_events_and_update_state() - { - var command = new UploadAppImage { File = new AssetFile("image.png", "image/png", 100, () => null) }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal("image/png", sut.Snapshot.Image.MimeType); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppImageUploaded { Image = sut.Snapshot.Image }) - ); - } - - [Fact] - public async Task RemoveImage_should_create_events_and_update_state() - { - var command = new RemoveAppImage(); - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Null(sut.Snapshot.Image); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppImageRemoved()) - ); - } - - [Fact] - public async Task ChangePlan_should_create_events_and_update_state() - { - var command = new ChangePlan { PlanId = planIdPaid }; - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) - .Returns(new PlanChangedResult()); - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - Assert.True(result.Value is PlanChangedResult); - - Assert.Equal(planIdPaid, sut.Snapshot.Plan.PlanId); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppPlanChanged { PlanId = planIdPaid }) - ); - } - - [Fact] - public async Task ChangePlan_should_reset_plan_for_reset_plan() - { - var command = new ChangePlan { PlanId = planIdFree }; - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) - .Returns(new PlanChangedResult()); - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdFree)) - .Returns(new PlanResetResult()); - - await ExecuteCreateAsync(); - await ExecuteChangePlanAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - Assert.True(result.Value is PlanResetResult); - - Assert.Null(sut.Snapshot.Plan); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppPlanReset()) - ); - } - - [Fact] - public async Task ChangePlan_should_not_make_update_for_redirect_result() - { - var command = new ChangePlan { PlanId = planIdPaid }; - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) - .Returns(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); - - Assert.Null(sut.Snapshot.Plan); - } - - [Fact] - public async Task ChangePlan_should_not_call_billing_manager_for_callback() - { - var command = new ChangePlan { PlanId = planIdPaid, FromCallback = true }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(new EntitySavedResult(5)); - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task AssignContributor_should_create_events_and_update_state() - { - var command = new AssignContributor { ContributorId = contributorId, Role = Role.Editor }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Role.Editor, sut.Snapshot.Contributors[contributorId]); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Editor, IsAdded = true }) - ); - } - - [Fact] - public async Task AssignContributor_should_create_update_events_and_update_state() - { - var command = new AssignContributor { ContributorId = contributorId, Role = Role.Owner }; - - await ExecuteCreateAsync(); - await ExecuteAssignContributorAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Role.Owner, sut.Snapshot.Contributors[contributorId]); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Owner }) - ); - } - - [Fact] - public async Task RemoveContributor_should_create_events_and_update_state() - { - var command = new RemoveContributor { ContributorId = contributorId }; - - await ExecuteCreateAsync(); - await ExecuteAssignContributorAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) - ); - } - - [Fact] - public async Task AttachClient_should_create_events_and_update_state() - { - var command = new AttachClient { Id = clientId }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.True(sut.Snapshot.Clients.ContainsKey(clientId)); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) - ); - } - - [Fact] - public async Task UpdateClient_should_create_events_and_update_state() - { - var command = new UpdateClient { Id = clientId, Name = clientNewName, Role = Role.Developer }; - - await ExecuteCreateAsync(); - await ExecuteAttachClientAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), - CreateEvent(new AppClientUpdated { Id = clientId, Role = Role.Developer }) - ); - } - - [Fact] - public async Task RevokeClient_should_create_events_and_update_state() - { - var command = new RevokeClient { Id = clientId }; - - await ExecuteCreateAsync(); - await ExecuteAttachClientAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.False(sut.Snapshot.Clients.ContainsKey(clientId)); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppClientRevoked { Id = clientId }) - ); - } - - [Fact] - public async Task AddWorkflow_should_create_events_and_update_state() - { - var command = new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.NotEmpty(sut.Snapshot.Workflows); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppWorkflowAdded { WorkflowId = workflowId, Name = "my-workflow" }) - ); - } - - [Fact] - public async Task UpdateWorkflow_should_create_events_and_update_state() - { - var command = new UpdateWorkflow { WorkflowId = workflowId, Workflow = Workflow.Default }; - - await ExecuteCreateAsync(); - await ExecuteAddWorkflowAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.NotEmpty(sut.Snapshot.Workflows); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppWorkflowUpdated { WorkflowId = workflowId, Workflow = Workflow.Default }) - ); - } - - [Fact] - public async Task DeleteWorkflow_should_create_events_and_update_state() - { - var command = new DeleteWorkflow { WorkflowId = workflowId }; - - await ExecuteCreateAsync(); - await ExecuteAddWorkflowAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Empty(sut.Snapshot.Workflows); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppWorkflowDeleted { WorkflowId = workflowId }) - ); - } - - [Fact] - public async Task AddLanguage_should_create_events_and_update_state() - { - var command = new AddLanguage { Language = Language.DE }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageAdded { Language = Language.DE }) - ); - } - - [Fact] - public async Task RemoveLanguage_should_create_events_and_update_state() - { - var command = new RemoveLanguage { Language = Language.DE }; - - await ExecuteCreateAsync(); - await ExecuteAddLanguageAsync(Language.DE); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.False(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageRemoved { Language = Language.DE }) - ); - } - - [Fact] - public async Task UpdateLanguage_should_create_events_and_update_state() - { - var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } }; - - await ExecuteCreateAsync(); - await ExecuteAddLanguageAsync(Language.DE); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List { Language.EN } }) - ); - } - - [Fact] - public async Task AddRole_should_create_events_and_update_state() - { - var command = new AddRole { Name = roleName }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(1, sut.Snapshot.Roles.CustomCount); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppRoleAdded { Name = roleName }) - ); - } - - [Fact] - public async Task DeleteRole_should_create_events_and_update_state() - { - var command = new DeleteRole { Name = roleName }; - - await ExecuteCreateAsync(); - await ExecuteAddRoleAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(0, sut.Snapshot.Roles.CustomCount); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppRoleDeleted { Name = roleName }) - ); - } - - [Fact] - public async Task UpdateRole_should_create_events_and_update_state() - { - var command = new UpdateRole { Name = roleName, Permissions = new[] { "clients.read" } }; - - await ExecuteCreateAsync(); - await ExecuteAddRoleAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppRoleUpdated { Name = roleName, Permissions = new[] { "clients.read" } }) - ); - } - - [Fact] - public async Task AddPattern_should_create_events_and_update_state() - { - var command = new AddPattern { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(initialPatterns.Count + 1, sut.Snapshot.Patterns.Count); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppPatternAdded { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }) - ); - } - - [Fact] - public async Task DeletePattern_should_create_events_and_update_state() - { - var command = new DeletePattern { PatternId = patternId3 }; - - await ExecuteCreateAsync(); - await ExecuteAddPatternAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(initialPatterns.Count, sut.Snapshot.Patterns.Count); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppPatternDeleted { PatternId = patternId3 }) - ); - } - - [Fact] - public async Task UpdatePattern_should_create_events_and_update_state() - { - var command = new UpdatePattern { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }; - - await ExecuteCreateAsync(); - await ExecuteAddPatternAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppPatternUpdated { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }) - ); - } - - [Fact] - public async Task ArchiveApp_should_create_events_and_update_state() - { - var command = new ArchiveApp(); - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(new EntitySavedResult(5)); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppArchived()) - ); - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, AppNamedId, null)) - .MustHaveHappened(); - } - - private Task ExecuteAddPatternAsync() - { - return sut.ExecuteAsync(CreateCommand(new AddPattern { PatternId = patternId3, Name = "Name", Pattern = ".*" })); - } - - private Task ExecuteCreateAsync() - { - return sut.ExecuteAsync(CreateCommand(new CreateApp { Name = AppName })); - } - - private Task ExecuteAssignContributorAsync() - { - return sut.ExecuteAsync(CreateCommand(new AssignContributor { ContributorId = contributorId, Role = Role.Editor })); - } - - private Task ExecuteAttachClientAsync() - { - return sut.ExecuteAsync(CreateCommand(new AttachClient { Id = clientId })); - } - - private Task ExecuteAddRoleAsync() - { - return sut.ExecuteAsync(CreateCommand(new AddRole { Name = roleName })); - } - - private Task ExecuteAddLanguageAsync(Language language) - { - return sut.ExecuteAsync(CreateCommand(new AddLanguage { Language = language })); - } - - private Task ExecuteAddWorkflowAsync() - { - return sut.ExecuteAsync(CreateCommand(new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" })); - } - - private Task ExecuteChangePlanAsync() - { - return sut.ExecuteAsync(CreateCommand(new ChangePlan { PlanId = planIdPaid })); - } - - private Task ExecuteArchiveAsync() - { - return sut.ExecuteAsync(CreateCommand(new ArchiveApp())); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs deleted file mode 100644 index 547db7299..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Billing -{ - public class NoopAppPlanBillingManagerTests - { - private readonly NoopAppPlanBillingManager sut = new NoopAppPlanBillingManager(); - - [Fact] - public void Should_not_have_portal() - { - Assert.False(sut.HasPortal); - } - - [Fact] - public async Task Should_do_nothing_when_changing_plan() - { - await sut.ChangePlanAsync(null, null, null); - } - - [Fact] - public async Task Should_not_return_portal_link() - { - Assert.Equal(string.Empty, await sut.GetPortalLinkAsync(null)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs deleted file mode 100644 index da4322d27..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs +++ /dev/null @@ -1,223 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Squidex.Shared.Users; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public class GuardAppContributorsTests - { - private readonly IUser user1 = A.Fake(); - private readonly IUser user2 = A.Fake(); - private readonly IUser user3 = A.Fake(); - private readonly IUserResolver users = A.Fake(); - private readonly IAppLimitsPlan appPlan = A.Fake(); - private readonly AppContributors contributors_0 = AppContributors.Empty; - private readonly Roles roles = Roles.Empty; - - public GuardAppContributorsTests() - { - A.CallTo(() => user1.Id).Returns("1"); - A.CallTo(() => user2.Id).Returns("2"); - A.CallTo(() => user3.Id).Returns("3"); - - A.CallTo(() => users.FindByIdOrEmailAsync("1")).Returns(user1); - A.CallTo(() => users.FindByIdOrEmailAsync("2")).Returns(user2); - A.CallTo(() => users.FindByIdOrEmailAsync("3")).Returns(user3); - - A.CallTo(() => users.FindByIdOrEmailAsync("1@email.com")).Returns(user1); - A.CallTo(() => users.FindByIdOrEmailAsync("2@email.com")).Returns(user2); - A.CallTo(() => users.FindByIdOrEmailAsync("3@email.com")).Returns(user3); - - A.CallTo(() => users.FindByIdOrEmailAsync("notfound")) - .Returns(Task.FromResult(null)); - - A.CallTo(() => appPlan.MaxContributors) - .Returns(10); - } - - [Fact] - public async Task CanAssign_should_throw_exception_if_contributor_id_is_null() - { - var command = new AssignContributor(); - - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan), - new ValidationError("Contributor id is required.", "ContributorId")); - } - - [Fact] - public async Task CanAssign_should_throw_exception_if_role_not_valid() - { - var command = new AssignContributor { ContributorId = "1", Role = "Invalid" }; - - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan), - new ValidationError("Role is not a valid value.", "Role")); - } - - [Fact] - public async Task CanAssign_should_throw_exception_if_user_already_exists_with_same_role() - { - var command = new AssignContributor { ContributorId = "1", Role = Role.Owner }; - - var contributors_1 = contributors_0.Assign("1", Role.Owner); - - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan), - new ValidationError("Contributor has already this role.", "Role")); - } - - [Fact] - public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_some_role_but_is_from_restore() - { - var command = new AssignContributor { ContributorId = "1", Role = Role.Owner, IsRestore = true }; - - var contributors_1 = contributors_0.Assign("1", Role.Owner); - - await GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan); - } - - [Fact] - public async Task CanAssign_should_throw_exception_if_user_not_found() - { - var command = new AssignContributor { ContributorId = "notfound", Role = Role.Owner }; - - await Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan)); - } - - [Fact] - public async Task CanAssign_should_throw_exception_if_user_is_actor() - { - var command = new AssignContributor { ContributorId = "3", Role = Role.Editor, Actor = new RefToken("user", "3") }; - - await Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan)); - } - - [Fact] - public async Task CanAssign_should_throw_exception_if_contributor_max_reached() - { - A.CallTo(() => appPlan.MaxContributors) - .Returns(2); - - var command = new AssignContributor { ContributorId = "3" }; - - var contributors_1 = contributors_0.Assign("1", Role.Owner); - var contributors_2 = contributors_1.Assign("2", Role.Editor); - - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan), - new ValidationError("You have reached the maximum number of contributors for your plan.")); - } - - [Fact] - public async Task CanAssign_assign_if_if_user_added_by_email() - { - var command = new AssignContributor { ContributorId = "1@email.com" }; - - await GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan); - - Assert.Equal("1", command.ContributorId); - } - - [Fact] - public async Task CanAssign_should_not_throw_exception_if_user_found() - { - A.CallTo(() => appPlan.MaxContributors) - .Returns(-1); - - var command = new AssignContributor { ContributorId = "1" }; - - await GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan); - } - - [Fact] - public async Task CanAssign_should_not_throw_exception_if_contributor_has_another_role() - { - var command = new AssignContributor { ContributorId = "1" }; - - var contributors_1 = contributors_0.Assign("1", Role.Developer); - - await GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan); - } - - [Fact] - public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_role_changed() - { - A.CallTo(() => appPlan.MaxContributors) - .Returns(2); - - var command = new AssignContributor { ContributorId = "1" }; - - var contributors_1 = contributors_0.Assign("1", Role.Developer); - var contributors_2 = contributors_1.Assign("2", Role.Developer); - - await GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan); - } - - [Fact] - public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_from_restore() - { - A.CallTo(() => appPlan.MaxContributors) - .Returns(2); - - var command = new AssignContributor { ContributorId = "3", IsRestore = true }; - - var contributors_1 = contributors_0.Assign("1", Role.Editor); - var contributors_2 = contributors_1.Assign("2", Role.Editor); - - await GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan); - } - - [Fact] - public void CanRemove_should_throw_exception_if_contributor_id_is_null() - { - var command = new RemoveContributor(); - - ValidationAssert.Throws(() => GuardAppContributors.CanRemove(contributors_0, command), - new ValidationError("Contributor id is required.", "ContributorId")); - } - - [Fact] - public void CanRemove_should_throw_exception_if_contributor_not_found() - { - var command = new RemoveContributor { ContributorId = "1" }; - - Assert.Throws(() => GuardAppContributors.CanRemove(contributors_0, command)); - } - - [Fact] - public void CanRemove_should_throw_exception_if_contributor_is_only_owner() - { - var command = new RemoveContributor { ContributorId = "1" }; - - var contributors_1 = contributors_0.Assign("1", Role.Owner); - var contributors_2 = contributors_1.Assign("2", Role.Editor); - - ValidationAssert.Throws(() => GuardAppContributors.CanRemove(contributors_2, command), - new ValidationError("Cannot remove the only owner.")); - } - - [Fact] - public void CanRemove_should_not_throw_exception_if_contributor_not_only_owner() - { - var command = new RemoveContributor { ContributorId = "1" }; - - var contributors_1 = contributors_0.Assign("1", Role.Owner); - var contributors_2 = contributors_1.Assign("2", Role.Owner); - - GuardAppContributors.CanRemove(contributors_2, command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs deleted file mode 100644 index 88b7093ac..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs +++ /dev/null @@ -1,165 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public class GuardAppRolesTests - { - private readonly string roleName = "Role1"; - private readonly Roles roles_0 = Roles.Empty; - private readonly AppContributors contributors = AppContributors.Empty; - private readonly AppClients clients = AppClients.Empty; - - [Fact] - public void CanAdd_should_throw_exception_if_name_empty() - { - var command = new AddRole { Name = null }; - - ValidationAssert.Throws(() => GuardAppRoles.CanAdd(roles_0, command), - new ValidationError("Name is required.", "Name")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_name_exists() - { - var roles_1 = roles_0.Add(roleName); - - var command = new AddRole { Name = roleName }; - - ValidationAssert.Throws(() => GuardAppRoles.CanAdd(roles_1, command), - new ValidationError("A role with the same name already exists.")); - } - - [Fact] - public void CanAdd_should_not_throw_exception_if_command_is_valid() - { - var command = new AddRole { Name = roleName }; - - GuardAppRoles.CanAdd(roles_0, command); - } - - [Fact] - public void CanDelete_should_throw_exception_if_name_empty() - { - var command = new DeleteRole { Name = null }; - - ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_0, command, contributors, clients), - new ValidationError("Name is required.", "Name")); - } - - [Fact] - public void CanDelete_should_throw_exception_if_role_not_found() - { - var command = new DeleteRole { Name = roleName }; - - Assert.Throws(() => GuardAppRoles.CanDelete(roles_0, command, contributors, clients)); - } - - [Fact] - public void CanDelete_should_throw_exception_if_contributor_found() - { - var roles_1 = roles_0.Add(roleName); - - var command = new DeleteRole { Name = roleName }; - - ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors.Assign("1", roleName), clients), - new ValidationError("Cannot remove a role when a contributor is assigned.")); - } - - [Fact] - public void CanDelete_should_throw_exception_if_client_found() - { - var roles_1 = roles_0.Add(roleName); - - var command = new DeleteRole { Name = roleName }; - - ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors, clients.Add("1", new AppClient("client", "1", roleName))), - new ValidationError("Cannot remove a role when a client is assigned.")); - } - - [Fact] - public void CanDelete_should_throw_exception_if_default_role() - { - var roles_1 = roles_0.Add(Role.Developer); - - var command = new DeleteRole { Name = Role.Developer }; - - ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors, clients), - new ValidationError("Cannot delete a default role.")); - } - - [Fact] - public void CanDelete_should_not_throw_exception_if_command_is_valid() - { - var roles_1 = roles_0.Add(roleName); - - var command = new DeleteRole { Name = roleName }; - - GuardAppRoles.CanDelete(roles_1, command, contributors, clients); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_name_empty() - { - var roles_1 = roles_0.Add(roleName); - - var command = new UpdateRole { Name = null, Permissions = new[] { "P1" } }; - - ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(roles_1, command), - new ValidationError("Name is required.", "Name")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_permission_is_null() - { - var roles_1 = roles_0.Add(roleName); - - var command = new UpdateRole { Name = roleName, Permissions = null }; - - ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(roles_1, command), - new ValidationError("Permissions is required.", "Permissions")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_default_role() - { - var roles_1 = roles_0.Add(Role.Developer); - - var command = new UpdateRole { Name = Role.Developer, Permissions = new[] { "P1" } }; - - ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(roles_1, command), - new ValidationError("Cannot update a default role.")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_role_does_not_exists() - { - var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } }; - - Assert.Throws(() => GuardAppRoles.CanUpdate(roles_0, command)); - } - - [Fact] - public void CanUpdate_should_not_throw_exception_if_role_exist_with_valid_command() - { - var roles_1 = roles_0.Add(roleName); - - var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } }; - - GuardAppRoles.CanUpdate(roles_1, command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs deleted file mode 100644 index 39513d134..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Validation; -using Squidex.Shared.Users; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public class GuardAppTests - { - private readonly IUserResolver users = A.Fake(); - private readonly IAppPlansProvider appPlans = A.Fake(); - private readonly IAppLimitsPlan basicPlan = A.Fake(); - private readonly IAppLimitsPlan freePlan = A.Fake(); - - public GuardAppTests() - { - A.CallTo(() => users.FindByIdOrEmailAsync(A.Ignored)) - .Returns(A.Dummy()); - - A.CallTo(() => appPlans.GetPlan("notfound")) - .Returns(null); - - A.CallTo(() => appPlans.GetPlan("basic")) - .Returns(basicPlan); - - A.CallTo(() => appPlans.GetPlan("free")) - .Returns(freePlan); - } - - [Fact] - public void CanCreate_should_throw_exception_if_name_not_valid() - { - var command = new CreateApp { Name = "INVALID NAME" }; - - ValidationAssert.Throws(() => GuardApp.CanCreate(command), - new ValidationError("Name is not a valid slug.", "Name")); - } - - [Fact] - public void CanCreate_should_not_throw_exception_if_app_name_is_valid() - { - var command = new CreateApp { Name = "new-app" }; - - GuardApp.CanCreate(command); - } - - [Fact] - public void CanUploadImage_should_throw_exception_if_name_not_valid() - { - var command = new UploadAppImage(); - - ValidationAssert.Throws(() => GuardApp.CanUploadImage(command), - new ValidationError("File is required.", "File")); - } - - [Fact] - public void CanUploadImage_should_not_throw_exception_if_app_name_is_valid() - { - var command = new UploadAppImage { File = new AssetFile("file.png", "image/png", 100, () => null) }; - - GuardApp.CanUploadImage(command); - } - - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_id_is_null() - { - var command = new ChangePlan { Actor = new RefToken("user", "me") }; - - AppPlan plan = null; - - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), - new ValidationError("Plan id is required.", "PlanId")); - } - - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_not_found() - { - var command = new ChangePlan { PlanId = "notfound", Actor = new RefToken("user", "me") }; - - AppPlan plan = null; - - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), - new ValidationError("A plan with this id does not exist.", "PlanId")); - } - - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_was_configured_from_another_user() - { - var command = new ChangePlan { PlanId = "basic", Actor = new RefToken("user", "me") }; - - var plan = new AppPlan(new RefToken("user", "other"), "premium"); - - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), - new ValidationError("Plan can only changed from the user who configured the plan initially.")); - } - - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_is_the_same() - { - var command = new ChangePlan { PlanId = "basic", Actor = new RefToken("user", "me") }; - - var plan = new AppPlan(command.Actor, "basic"); - - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), - new ValidationError("App has already this plan.")); - } - - [Fact] - public void CanChangePlan_should_not_throw_exception_if_same_user_but_other_plan() - { - var command = new ChangePlan { PlanId = "basic", Actor = new RefToken("user", "me") }; - - var plan = new AppPlan(command.Actor, "premium"); - - GuardApp.CanChangePlan(command, plan, appPlans); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs deleted file mode 100644 index b22b3e030..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs +++ /dev/null @@ -1,213 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public class GuardAppWorkflowTests - { - private readonly Guid workflowId = Guid.NewGuid(); - private readonly Workflows workflows; - - public GuardAppWorkflowTests() - { - workflows = Workflows.Empty.Add(workflowId, "name"); - } - - [Fact] - public void CanAdd_should_throw_exception_if_name_is_not_defined() - { - var command = new AddWorkflow(); - - ValidationAssert.Throws(() => GuardAppWorkflows.CanAdd(command), - new ValidationError("Name is required.", "Name")); - } - - [Fact] - public void CanAdd_should_not_throw_exception_if_command_is_valid() - { - var command = new AddWorkflow { Name = "my-workflow" }; - - GuardAppWorkflows.CanAdd(command); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_not_found() - { - var command = new UpdateWorkflow - { - Workflow = Workflow.Empty, - WorkflowId = Guid.NewGuid() - }; - - Assert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command)); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_is_not_defined() - { - var command = new UpdateWorkflow { WorkflowId = workflowId }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), - new ValidationError("Workflow is required.", "Workflow")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_has_no_initial_step() - { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - default, - new Dictionary - { - [Status.Published] = new WorkflowStep() - }), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), - new ValidationError("Initial step is required.", "Workflow.Initial")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_initial_step_is_published() - { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Published, - new Dictionary - { - [Status.Published] = new WorkflowStep() - }), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), - new ValidationError("Initial step cannot be published step.", "Workflow.Initial")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_does_not_have_published_state() - { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Draft, - new Dictionary - { - [Status.Draft] = new WorkflowStep() - }), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), - new ValidationError("Workflow must have a published step.", "Workflow.Steps")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_step_is_not_defined() - { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Draft, - new Dictionary - { - [Status.Published] = null, - [Status.Draft] = new WorkflowStep() - }), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), - new ValidationError("Step is required.", "Workflow.Steps.Published")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_transition_is_invalid() - { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Draft, - new Dictionary - { - [Status.Published] = - new WorkflowStep( - new Dictionary - { - [Status.Archived] = new WorkflowTransition() - }), - [Status.Draft] = new WorkflowStep() - }), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), - new ValidationError("Transition has an invalid target.", "Workflow.Steps.Published.Transitions.Archived")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_transition_is_not_defined() - { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Draft, - new Dictionary - { - [Status.Draft] = - new WorkflowStep(), - [Status.Published] = - new WorkflowStep( - new Dictionary - { - [Status.Draft] = null - }) - }), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), - new ValidationError("Transition is required.", "Workflow.Steps.Published.Transitions.Draft")); - } - - [Fact] - public void CanUpdate_should_not_throw_exception_if_workflow_is_valid() - { - var command = new UpdateWorkflow { Workflow = Workflow.Default, WorkflowId = workflowId }; - - GuardAppWorkflows.CanUpdate(workflows, command); - } - - [Fact] - public void CanDelete_should_throw_exception_if_workflow_not_found() - { - var command = new DeleteWorkflow { WorkflowId = Guid.NewGuid() }; - - Assert.Throws(() => GuardAppWorkflows.CanDelete(workflows, command)); - } - - [Fact] - public void CanDelete_should_not_throw_exception_if_workflow_is_found() - { - var command = new DeleteWorkflow { WorkflowId = workflowId }; - - GuardAppWorkflows.CanDelete(workflows, command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs deleted file mode 100644 index e253c5ddc..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs +++ /dev/null @@ -1,387 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public sealed class AppsIndexTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly IAppsByNameIndexGrain indexByName = A.Fake(); - private readonly IAppsByUserIndexGrain indexByUser = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly string userId = "user-1"; - private readonly AppsIndex sut; - - public AppsIndexTests() - { - A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) - .Returns(indexByName); - - A.CallTo(() => grainFactory.GetGrain(userId, null)) - .Returns(indexByUser); - - sut = new AppsIndex(grainFactory); - } - - [Fact] - public async Task Should_resolve_all_apps_from_user_permissions() - { - var expected = SetupApp(0, false); - - A.CallTo(() => indexByName.GetIdsAsync(A.That.IsSameSequenceAs(new string[] { appId.Name }))) - .Returns(new List { appId.Id }); - - var actual = await sut.GetAppsForUserAsync(userId, new PermissionSet($"squidex.apps.{appId.Name}")); - - Assert.Same(expected, actual[0]); - } - - [Fact] - public async Task Should_resolve_all_apps_from_user() - { - var expected = SetupApp(0, false); - - A.CallTo(() => indexByUser.GetIdsAsync()) - .Returns(new List { appId.Id }); - - var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty); - - Assert.Same(expected, actual[0]); - } - - [Fact] - public async Task Should_resolve_all_apps() - { - var expected = SetupApp(0, false); - - A.CallTo(() => indexByName.GetIdsAsync()) - .Returns(new List { appId.Id }); - - var actual = await sut.GetAppsAsync(); - - Assert.Same(expected, actual[0]); - } - - [Fact] - public async Task Should_resolve_app_by_name() - { - var expected = SetupApp(0, false); - - A.CallTo(() => indexByName.GetIdAsync(appId.Name)) - .Returns(appId.Id); - - var actual = await sut.GetAppByNameAsync(appId.Name); - - Assert.Same(expected, actual); - } - - [Fact] - public async Task Should_resolve_app_by_id() - { - var expected = SetupApp(0, false); - - var actual = await sut.GetAppAsync(appId.Id); - - Assert.Same(expected, actual); - } - - [Fact] - public async Task Should_return_null_if_app_archived() - { - SetupApp(0, true); - - var actual = await sut.GetAppAsync(appId.Id); - - Assert.Null(actual); - } - - [Fact] - public async Task Should_return_null_if_app_not_created() - { - SetupApp(-1, false); - - var actual = await sut.GetAppAsync(appId.Id); - - Assert.Null(actual); - } - - [Fact] - public async Task Should_add_app_to_indexes_on_create() - { - var token = RandomHash.Simple(); - - A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) - .Returns(token); - - var context = - new CommandContext(Create(appId.Name), commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByName.AddAsync(token)) - .MustHaveHappened(); - - A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByUser.AddAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_app_to_user_index_if_app_created_by_client() - { - var token = RandomHash.Simple(); - - A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) - .Returns(token); - - var context = - new CommandContext(CreateFromClient(appId.Name), commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByName.AddAsync(token)) - .MustHaveHappened(); - - A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByUser.AddAsync(appId.Id)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_clear_reservation_when_app_creation_failed() - { - var token = RandomHash.Simple(); - - A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) - .Returns(token); - - var context = - new CommandContext(CreateFromClient(appId.Name), commandBus); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByName.AddAsync(token)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByName.RemoveReservationAsync(token)) - .MustHaveHappened(); - - A.CallTo(() => indexByUser.AddAsync(appId.Id)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_add_to_indexes_on_create_if_name_taken() - { - A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) - .Returns(Task.FromResult(null)); - - var context = - new CommandContext(Create(appId.Name), commandBus) - .Complete(); - - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - - A.CallTo(() => indexByName.AddAsync(A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByUser.AddAsync(appId.Id)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_add_to_indexes_on_create_if_name_invalid() - { - var context = - new CommandContext(Create("INVALID"), commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByName.ReserveAsync(appId.Id, A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByUser.AddAsync(appId.Id)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_add_app_to_index_on_contributor_assignment() - { - var context = - new CommandContext(new AssignContributor { AppId = appId.Id, ContributorId = userId }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByUser.AddAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_remove_from_user_index_on_remove_of_contributor() - { - var context = - new CommandContext(new RemoveContributor { AppId = appId.Id, ContributorId = userId }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByUser.RemoveAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_remove_app_from_indexes_on_archive() - { - var app = SetupApp(0, false); - - var context = - new CommandContext(new ArchiveApp { AppId = appId.Id }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByName.RemoveAsync(appId.Id)) - .MustHaveHappened(); - - A.CallTo(() => indexByUser.RemoveAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_call_when_rebuilding_for_contributors1() - { - var apps = new HashSet(); - - await sut.RebuildByContributorsAsync(userId, apps); - - A.CallTo(() => indexByUser.RebuildAsync(apps)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_call_when_rebuilding_for_contributors2() - { - var users = new HashSet { userId }; - - await sut.RebuildByContributorsAsync(appId.Id, users); - - A.CallTo(() => indexByUser.AddAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_call_when_rebuilding() - { - var apps = new Dictionary(); - - await sut.RebuildAsync(apps); - - A.CallTo(() => indexByName.RebuildAsync(apps)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_reserveration() - { - await sut.AddAsync("token"); - - A.CallTo(() => indexByName.AddAsync("token")) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_remove_reservation() - { - await sut.RemoveReservationAsync("token"); - - A.CallTo(() => indexByName.RemoveReservationAsync("token")) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_request_for_ids() - { - await sut.GetIdsAsync(); - - A.CallTo(() => indexByName.GetIdsAsync()) - .MustHaveHappened(); - } - - private IAppEntity SetupApp(long version, bool archived) - { - var appEntity = A.Fake(); - - A.CallTo(() => appEntity.Name) - .Returns(appId.Name); - A.CallTo(() => appEntity.Version) - .Returns(version); - A.CallTo(() => appEntity.IsArchived) - .Returns(archived); - A.CallTo(() => appEntity.Contributors) - .Returns(AppContributors.Empty.Assign(userId, Role.Owner)); - - var appGrain = A.Fake(); - - A.CallTo(() => appGrain.GetStateAsync()) - .Returns(J.Of(appEntity)); - - A.CallTo(() => grainFactory.GetGrain(appId.Id, null)) - .Returns(appGrain); - - return appEntity; - } - - private CreateApp Create(string name) - { - return new CreateApp { AppId = appId.Id, Name = name, Actor = ActorSubject() }; - } - - private CreateApp CreateFromClient(string name) - { - return new CreateApp { AppId = appId.Id, Name = name, Actor = ActorClient() }; - } - - private RefToken ActorSubject() - { - return new RefToken(RefTokenType.Subject, userId); - } - - private RefToken ActorClient() - { - return new RefToken(RefTokenType.Client, userId); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs deleted file mode 100644 index bcb311f9a..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs +++ /dev/null @@ -1,149 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure.EventSourcing; -using Xunit; - -#pragma warning disable SA1401 // Fields must be private - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public class AssetChangedTriggerHandlerTests - { - private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly IAssetLoader assetLoader = A.Fake(); - private readonly IRuleTriggerHandler sut; - - public AssetChangedTriggerHandlerTests() - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) - .Returns(true); - - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) - .Returns(false); - - sut = new AssetChangedTriggerHandler(scriptEngine, assetLoader); - } - - public static IEnumerable TestEvents = new[] - { - new object[] { new AssetCreated(), EnrichedAssetEventType.Created }, - new object[] { new AssetUpdated(), EnrichedAssetEventType.Updated }, - new object[] { new AssetAnnotated(), EnrichedAssetEventType.Annotated }, - new object[] { new AssetDeleted(), EnrichedAssetEventType.Deleted } - }; - - [Theory] - [MemberData(nameof(TestEvents))] - public async Task Should_enrich_events(AssetEvent @event, EnrichedAssetEventType type) - { - var envelope = Envelope.Create(@event).SetEventStreamNumber(12); - - A.CallTo(() => assetLoader.GetAsync(@event.AssetId, 12)) - .Returns(new AssetEntity()); - - var result = await sut.CreateEnrichedEventAsync(envelope); - - Assert.Equal(type, ((EnrichedAssetEvent)result).Type); - } - - [Fact] - public void Should_not_trigger_precheck_when_event_type_not_correct() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new ContentCreated(), trigger, Guid.NewGuid()); - - Assert.False(result); - }); - } - - [Fact] - public void Should_trigger_precheck_when_event_type_correct() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new AssetCreated(), trigger, Guid.NewGuid()); - - Assert.True(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_event_type_not_correct() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new EnrichedContentEvent(), trigger); - - Assert.False(result); - }); - } - - [Fact] - public void Should_trigger_check_when_condition_is_empty() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new EnrichedAssetEvent(), trigger); - - Assert.True(result); - }); - } - - [Fact] - public void Should_trigger_check_when_condition_matchs() - { - TestForCondition("true", trigger => - { - var result = sut.Trigger(new EnrichedAssetEvent(), trigger); - - Assert.True(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_condition_does_not_matchs() - { - TestForCondition("false", trigger => - { - var result = sut.Trigger(new EnrichedAssetEvent(), trigger); - - Assert.False(result); - }); - } - - private void TestForCondition(string condition, Action action) - { - var trigger = new AssetChangedTriggerV2 { Condition = condition }; - - action(trigger); - - if (string.IsNullOrWhiteSpace(condition)) - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) - .MustNotHaveHappened(); - } - else - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) - .MustHaveHappened(); - } - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs deleted file mode 100644 index 272ba9c8c..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Infrastructure.Assets; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public class FileTypeTagGeneratorTests - { - private readonly HashSet tags = new HashSet(); - private readonly FileTypeTagGenerator sut = new FileTypeTagGenerator(); - - [Fact] - public void Should_not_add_tag_if_no_file_info() - { - var command = new CreateAsset(); - - sut.GenerateTags(command, tags); - - Assert.Empty(tags); - } - - [Fact] - public void Should_add_file_type() - { - var command = new CreateAsset - { - File = new AssetFile("File.DOCX", "Mime", 100, () => null) - }; - - sut.GenerateTags(command, tags); - - Assert.Contains("type/docx", tags); - } - - [Fact] - public void Should_add_blob_if_without_extension() - { - var command = new CreateAsset - { - File = new AssetFile("File", "Mime", 100, () => null) - }; - - sut.GenerateTags(command, tags); - - Assert.Contains("type/blob", tags); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs deleted file mode 100644 index 0a54832e7..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs +++ /dev/null @@ -1,236 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; -using FakeItEasy; -using MongoDB.Bson.Serialization; -using MongoDB.Driver; -using NodaTime.Text; -using Squidex.Domain.Apps.Entities.MongoDb.Assets; -using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.MongoDb.Queries; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Validation; -using Xunit; -using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; -using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; - -namespace Squidex.Domain.Apps.Entities.Assets.MongoDb -{ - public class MongoDbQueryTests - { - private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; - private static readonly IBsonSerializer Serializer = BsonSerializer.SerializerRegistry.GetSerializer(); - - static MongoDbQueryTests() - { - InstantSerializer.Register(); - } - - [Fact] - public void Should_throw_exception_for_full_text_search() - { - Assert.Throws(() => Q(new ClrQuery { FullText = "Full Text" })); - } - - [Fact] - public void Should_make_query_with_lastModified() - { - var i = F(ClrFilter.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - var o = C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_lastModifiedBy() - { - var i = F(ClrFilter.Eq("lastModifiedBy", "Me")); - var o = C("{ 'mb' : 'Me' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_created() - { - var i = F(ClrFilter.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - var o = C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_createdBy() - { - var i = F(ClrFilter.Eq("createdBy", "Me")); - var o = C("{ 'cb' : 'Me' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_version() - { - var i = F(ClrFilter.Eq("version", 0)); - var o = C("{ 'vs' : NumberLong(0) }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_fileVersion() - { - var i = F(ClrFilter.Eq("fileVersion", 2)); - var o = C("{ 'fv' : NumberLong(2) }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_tags() - { - var i = F(ClrFilter.Eq("tags", "tag1")); - var o = C("{ 'td' : 'tag1' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_fileName() - { - var i = F(ClrFilter.Eq("fileName", "Logo.png")); - var o = C("{ 'fn' : 'Logo.png' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_isImage() - { - var i = F(ClrFilter.Eq("isImage", true)); - var o = C("{ 'im' : true }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_mimeType() - { - var i = F(ClrFilter.Eq("mimeType", "text/json")); - var o = C("{ 'mm' : 'text/json' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_fileSize() - { - var i = F(ClrFilter.Eq("fileSize", 1024)); - var o = C("{ 'fs' : NumberLong(1024) }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_pixelHeight() - { - var i = F(ClrFilter.Eq("pixelHeight", 600)); - var o = C("{ 'ph' : 600 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_pixelWidth() - { - var i = F(ClrFilter.Eq("pixelWidth", 800)); - var o = C("{ 'pw' : 800 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_orderby_with_single_field() - { - var i = S(SortBuilder.Descending("lastModified")); - var o = C("{ 'mt' : -1 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_orderby_with_multiple_fields() - { - var i = S(SortBuilder.Ascending("lastModified"), SortBuilder.Descending("lastModifiedBy")); - var o = C("{ 'mt' : 1, 'mb' : -1 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_take_statement() - { - var query = new ClrQuery { Take = 3 }; - var cursor = A.Fake>(); - - cursor.AssetTake(query.AdjustToModel()); - - A.CallTo(() => cursor.Limit(3)) - .MustHaveHappened(); - } - - [Fact] - public void Should_make_skip_statement() - { - var query = new ClrQuery { Skip = 3 }; - var cursor = A.Fake>(); - - cursor.AssetSkip(query.AdjustToModel()); - - A.CallTo(() => cursor.Skip(3)) - .MustHaveHappened(); - } - - private static string C(string value) - { - return value.Replace('\'', '"'); - } - - private static string F(FilterNode filter) - { - return Q(new ClrQuery { Filter = filter }); - } - - private static string S(params SortNode[] sorts) - { - var cursor = A.Fake>(); - - var i = string.Empty; - - A.CallTo(() => cursor.Sort(A>.Ignored)) - .Invokes((SortDefinition sortDefinition) => - { - i = sortDefinition.Render(Serializer, Registry).ToString(); - }); - - cursor.AssetSort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel()); - - return i; - } - - private static string Q(ClrQuery query) - { - var rendered = - query.AdjustToModel().BuildFilter(false).Filter - .Render(Serializer, Registry).ToString(); - - return rendered; - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs deleted file mode 100644 index 373c252ed..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Assets.Queries -{ - public class AssetLoaderTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly IAssetGrain grain = A.Fake(); - private readonly Guid id = Guid.NewGuid(); - private readonly AssetLoader sut; - - public AssetLoaderTests() - { - A.CallTo(() => grainFactory.GetGrain(id, null)) - .Returns(grain); - - sut = new AssetLoader(grainFactory); - } - - [Fact] - public async Task Should_throw_exception_if_no_state_returned() - { - A.CallTo(() => grain.GetStateAsync(10)) - .Returns(J.Of(null)); - - await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); - } - - [Fact] - public async Task Should_throw_exception_if_state_has_other_version() - { - var content = new AssetEntity { Version = 5 }; - - A.CallTo(() => grain.GetStateAsync(10)) - .Returns(J.Of(content)); - - await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); - } - - [Fact] - public async Task Should_return_content_from_state() - { - var content = new AssetEntity { Version = 10 }; - - A.CallTo(() => grain.GetStateAsync(10)) - .Returns(J.Of(content)); - - var result = await sut.GetAsync(id, 10); - - Assert.Same(content, result); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs deleted file mode 100644 index f0da996fd..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure.Queries; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Assets.Queries -{ - public class FilterTagTransformerTests - { - private readonly ITagService tagService = A.Fake(); - private readonly Guid appId = Guid.NewGuid(); - - [Fact] - public void Should_normalize_tags() - { - A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A>.That.Contains("name1"))) - .Returns(new Dictionary { ["name1"] = "id1" }); - - var source = ClrFilter.Eq("tags", "name1"); - - var result = FilterTagTransformer.Transform(source, appId, tagService); - - Assert.Equal("tags == 'id1'", result.ToString()); - } - - [Fact] - public void Should_not_fail_when_tags_not_found() - { - A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A>.That.Contains("name1"))) - .Returns(new Dictionary()); - - var source = ClrFilter.Eq("tags", "name1"); - - var result = FilterTagTransformer.Transform(source, appId, tagService); - - Assert.Equal("tags == 'name1'", result.ToString()); - } - - [Fact] - public void Should_not_normalize_other_field() - { - var source = ClrFilter.Eq("other", "value"); - - var result = FilterTagTransformer.Transform(source, appId, tagService); - - Assert.Equal("other == 'value'", result.ToString()); - - A.CallTo(() => tagService.GetTagIdsAsync(appId, A.Ignored, A>.Ignored)) - .MustNotHaveHappened(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs deleted file mode 100644 index 980a44197..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs +++ /dev/null @@ -1,235 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Xunit; - -#pragma warning disable SA1401 // Fields must be private -#pragma warning disable RECS0070 - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class ContentChangedTriggerHandlerTests - { - private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly IContentLoader contentLoader = A.Fake(); - private readonly IRuleTriggerHandler sut; - private readonly Guid ruleId = Guid.NewGuid(); - private static readonly NamedId SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1"); - private static readonly NamedId SchemaNonMatch = NamedId.Of(Guid.NewGuid(), "my-schema2"); - - public ContentChangedTriggerHandlerTests() - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) - .Returns(true); - - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) - .Returns(false); - - sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader); - } - - public static IEnumerable TestEvents = new[] - { - new object[] { new ContentCreated(), EnrichedContentEventType.Created }, - new object[] { new ContentUpdated(), EnrichedContentEventType.Updated }, - new object[] { new ContentDeleted(), EnrichedContentEventType.Deleted }, - new object[] { new ContentStatusChanged { Change = StatusChange.Change }, EnrichedContentEventType.StatusChanged }, - new object[] { new ContentStatusChanged { Change = StatusChange.Published }, EnrichedContentEventType.Published }, - new object[] { new ContentStatusChanged { Change = StatusChange.Unpublished }, EnrichedContentEventType.Unpublished } - }; - - [Theory] - [MemberData(nameof(TestEvents))] - public async Task Should_enrich_events(ContentEvent @event, EnrichedContentEventType type) - { - var envelope = Envelope.Create(@event).SetEventStreamNumber(12); - - A.CallTo(() => contentLoader.GetAsync(@event.ContentId, 12)) - .Returns(new ContentEntity { SchemaId = SchemaMatch }); - - var result = await sut.CreateEnrichedEventAsync(envelope); - - Assert.Equal(type, ((EnrichedContentEvent)result).Type); - } - - [Fact] - public void Should_not_trigger_precheck_when_event_type_not_correct() - { - TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => - { - var result = sut.Trigger(new AssetCreated(), trigger, ruleId); - - Assert.False(result); - }); - } - - [Fact] - public void Should_not_trigger_precheck_when_trigger_contains_no_schemas() - { - TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => - { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); - - Assert.False(result); - }); - } - - [Fact] - public void Should_trigger_precheck_when_handling_all_events() - { - TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => - { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); - - Assert.True(result); - }); - } - - [Fact] - public void Should_trigger_precheck_when_condition_is_empty() - { - TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => - { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); - - Assert.True(result); - }); - } - - [Fact] - public void Should_not_trigger_precheck_when_schema_id_does_not_match() - { - TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => - { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); - - Assert.False(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_event_type_not_correct() - { - TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => - { - var result = sut.Trigger(new EnrichedAssetEvent(), trigger); - - Assert.False(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_trigger_contains_no_schemas() - { - TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => - { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); - - Assert.False(result); - }); - } - - [Fact] - public void Should_trigger_check_when_handling_all_events() - { - TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => - { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); - - Assert.True(result); - }); - } - - [Fact] - public void Should_trigger_check_when_condition_is_empty() - { - TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => - { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); - - Assert.True(result); - }); - } - - [Fact] - public void Should_trigger_check_when_condition_matchs() - { - TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "true", action: trigger => - { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); - - Assert.True(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_schema_id_does_not_match() - { - TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => - { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); - - Assert.False(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_condition_does_not_matchs() - { - TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "false", action: trigger => - { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); - - Assert.False(result); - }); - } - - private void TestForTrigger(bool handleAll, NamedId schemaId, string condition, Action action) - { - var trigger = new ContentChangedTriggerV2 { HandleAll = handleAll }; - - if (schemaId != null) - { - trigger.Schemas = new ReadOnlyCollection(new List - { - new ContentChangedTriggerSchemaV2 - { - SchemaId = schemaId.Id, Condition = condition - } - }); - } - - action(trigger); - - if (string.IsNullOrWhiteSpace(condition)) - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) - .MustNotHaveHappened(); - } - else - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) - .MustHaveHappened(); - } - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs deleted file mode 100644 index af8fc0bd6..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs +++ /dev/null @@ -1,592 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using NodaTime; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Contents.State; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class ContentGrainTests : HandlerTestBase - { - private readonly Guid contentId = Guid.NewGuid(); - private readonly IActivationLimit limit = A.Fake(); - private readonly IAppEntity app; - private readonly IAppProvider appProvider = A.Fake(); - private readonly IContentRepository contentRepository = A.Dummy(); - private readonly IContentWorkflow contentWorkflow = A.Fake(x => x.Wrapping(new DefaultContentWorkflow())); - private readonly ISchemaEntity schema; - private readonly IScriptEngine scriptEngine = A.Fake(); - - private readonly NamedContentData invalidData = - new NamedContentData() - .AddField("my-field1", - new ContentFieldData() - .AddValue("iv", null)) - .AddField("my-field2", - new ContentFieldData() - .AddValue("iv", 1)); - private readonly NamedContentData data = - new NamedContentData() - .AddField("my-field1", - new ContentFieldData() - .AddValue("iv", 1)); - private readonly NamedContentData patch = - new NamedContentData() - .AddField("my-field2", - new ContentFieldData() - .AddValue("iv", 2)); - private readonly NamedContentData otherData = - new NamedContentData() - .AddField("my-field1", - new ContentFieldData() - .AddValue("iv", 2)) - .AddField("my-field2", - new ContentFieldData() - .AddValue("iv", 2)); - private readonly NamedContentData patched; - private readonly ContentGrain sut; - - protected override Guid Id - { - get { return contentId; } - } - - public ContentGrainTests() - { - app = Mocks.App(AppNamedId, Language.DE); - - var scripts = new SchemaScripts - { - Change = "", - Create = "", - Delete = "", - Update = "" - }; - - var schemaDef = - new Schema("my-schema") - .AddNumber(1, "my-field1", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = true }) - .AddNumber(2, "my-field2", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = false }) - .ConfigureScripts(scripts); - - schema = Mocks.Schema(AppNamedId, SchemaNamedId, schemaDef); - - A.CallTo(() => appProvider.GetAppAsync(AppName)) - .Returns(app); - - A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)) - .Returns((app, schema)); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .ReturnsLazily(x => x.GetArgument(0).Data); - - patched = patch.MergeInto(data); - - sut = new ContentGrain(Store, A.Dummy(), appProvider, A.Dummy(), scriptEngine, contentWorkflow, contentRepository, limit); - sut.ActivateAsync(Id).Wait(); - } - - [Fact] - public void Should_set_limit() - { - A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5))) - .MustHaveHappened(); - } - - [Fact] - public async Task Command_should_throw_exception_if_content_is_deleted() - { - await ExecuteCreateAsync(); - await ExecuteDeleteAsync(); - - await Assert.ThrowsAsync(ExecuteUpdateAsync); - } - - [Fact] - public async Task Create_should_create_events_and_update_state() - { - var command = new CreateContent { Data = data }; - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Status.Draft, sut.Snapshot.Status); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) - ); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(data, null, Status.Draft), "")) - .MustHaveHappened(); - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Create_should_also_publish() - { - var command = new CreateContent { Data = data, Publish = true }; - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Status.Published, sut.Snapshot.Status); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }), - CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) - ); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(data, null, Status.Draft), "")) - .MustHaveHappened(); - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Published), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task Create_should_throw_when_invalid_data_is_passed() - { - var command = new CreateContent { Data = invalidData }; - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(CreateContentCommand(command))); - } - - [Fact] - public async Task Update_should_create_events_and_update_state() - { - var command = new UpdateContent { Data = otherData }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = otherData }) - ); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(otherData, data, Status.Draft), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task Update_should_create_proposal_events_and_update_state() - { - var command = new UpdateContent { Data = otherData, AsDraft = true }; - - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.True(sut.Snapshot.IsPending); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdateProposed { Data = otherData }) - ); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(otherData, data, Status.Published), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task Update_should_not_create_event_for_same_data() - { - var command = new UpdateContent { Data = data }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Single(LastEvents); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Update_should_throw_when_invalid_data_is_passed() - { - var command = new UpdateContent { Data = invalidData }; - - await ExecuteCreateAsync(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(CreateContentCommand(command))); - } - - [Fact] - public async Task Patch_should_create_events_and_update_state() - { - var command = new PatchContent { Data = patch }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = patched }) - ); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(patched, data, Status.Draft), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task Patch_should_create_proposal_events_and_update_state() - { - var command = new PatchContent { Data = patch, AsDraft = true }; - - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.True(sut.Snapshot.IsPending); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdateProposed { Data = patched }) - ); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(patched, data, Status.Published), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task Patch_should_not_create_event_for_same_data() - { - var command = new PatchContent { Data = data }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Single(LastEvents); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) - .MustNotHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_create_events_and_update_state() - { - var command = new ChangeContentStatus { Status = Status.Published }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Status.Published, sut.Snapshot.Status); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Published, Status = Status.Published }) - ); - - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Published, Status.Draft), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_create_events_and_update_state_when_archived() - { - var command = new ChangeContentStatus { Status = Status.Archived }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Status.Archived, sut.Snapshot.Status); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) - ); - - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Archived, Status.Draft), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_create_events_and_update_state_when_unpublished() - { - var command = new ChangeContentStatus { Status = Status.Draft }; - - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Status.Draft, sut.Snapshot.Status); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Unpublished, Status = Status.Draft }) - ); - - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft, Status.Published), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_create_events_and_update_state_when_restored() - { - var command = new ChangeContentStatus { Status = Status.Draft }; - - await ExecuteCreateAsync(); - await ExecuteArchiveAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Status.Draft, sut.Snapshot.Status); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Draft }) - ); - - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft, Status.Archived), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_create_proposal_events_and_update_state() - { - var command = new ChangeContentStatus { Status = Status.Published }; - - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - await ExecuteProposeUpdateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.False(sut.Snapshot.IsPending); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentChangesPublished()) - ); - - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) - .MustNotHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_refresh_properties_and_create_scheduled_events_when_command_has_due_time() - { - var dueTime = Instant.MaxValue; - - var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTime }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Status.Draft, sut.Snapshot.Status); - Assert.Equal(Status.Published, sut.Snapshot.ScheduleJob.Status); - Assert.Equal(dueTime, sut.Snapshot.ScheduleJob.DueTime); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime }) - ); - - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) - .MustNotHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_refresh_properties_and_revert_scheduling_when_invoked_by_scheduler() - { - await ExecuteCreateAsync(); - await ExecuteChangeStatusAsync(Status.Published, Instant.MaxValue); - - var command = new ChangeContentStatus { Status = Status.Published, JobId = sut.Snapshot.ScheduleJob.Id }; - - A.CallTo(() => contentWorkflow.CanMoveToAsync(A.Ignored, Status.Published, User)) - .Returns(false); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Null(sut.Snapshot.ScheduleJob); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentSchedulingCancelled()) - ); - - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Delete_should_update_properties_and_create_events() - { - var command = new DeleteContent(); - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(new EntitySavedResult(1)); - - Assert.True(sut.Snapshot.IsDeleted); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentDeleted()) - ); - - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task DiscardChanges_should_update_properties_and_create_events() - { - var command = new DiscardChanges(); - - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - await ExecuteProposeUpdateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(new EntitySavedResult(3)); - - Assert.False(sut.Snapshot.IsPending); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentChangesDiscarded()) - ); - } - - private Task ExecuteCreateAsync() - { - return sut.ExecuteAsync(CreateContentCommand(new CreateContent { Data = data })); - } - - private Task ExecuteUpdateAsync() - { - return sut.ExecuteAsync(CreateContentCommand(new UpdateContent { Data = otherData })); - } - - private Task ExecuteProposeUpdateAsync() - { - return sut.ExecuteAsync(CreateContentCommand(new UpdateContent { Data = otherData, AsDraft = true })); - } - - private Task ExecuteChangeStatusAsync(Status status, Instant? dueTime = null) - { - return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = status, DueTime = dueTime })); - } - - private Task ExecuteDeleteAsync() - { - return sut.ExecuteAsync(CreateContentCommand(new DeleteContent())); - } - - private Task ExecuteArchiveAsync() - { - return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Archived })); - } - - private Task ExecutePublishAsync() - { - return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); - } - - private ScriptContext ScriptContext(NamedContentData newData, NamedContentData oldData, Status newStatus) - { - return A.That.Matches(x => M(x, newData, oldData, newStatus, default)); - } - - private ScriptContext ScriptContext(NamedContentData newData, NamedContentData oldData, Status newStatus, Status oldStatus) - { - return A.That.Matches(x => M(x, newData, oldData, newStatus, oldStatus)); - } - - private bool M(ScriptContext x, NamedContentData newData, NamedContentData oldData, Status newStatus, Status oldStatus) - { - return - Equals(x.Data, newData) && - Equals(x.DataOld, oldData) && - Equals(x.Status, newStatus) && - Equals(x.StatusOld, oldStatus) && - x.ContentId == contentId && x.User == User; - } - - protected T CreateContentEvent(T @event) where T : ContentEvent - { - @event.ContentId = contentId; - - return CreateEvent(@event); - } - - protected T CreateContentCommand(T command) where T : ContentCommand - { - command.ContentId = contentId; - - return CreateCommand(command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs deleted file mode 100644 index a738581b2..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs +++ /dev/null @@ -1,139 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Contents; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class DefaultContentWorkflowTests - { - private readonly DefaultContentWorkflow sut = new DefaultContentWorkflow(); - - [Fact] - public async Task Should_always_allow_publish_on_create() - { - var result = await sut.CanPublishOnCreateAsync(null, null, null); - - Assert.True(result); - } - - [Fact] - public async Task Should_draft_as_initial_status() - { - var expected = new StatusInfo(Status.Draft, StatusColors.Draft); - - var result = await sut.GetInitialStatusAsync(null); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_check_is_valid_next() - { - var content = new ContentEntity { Status = Status.Published }; - - var result = await sut.CanMoveToAsync(content, Status.Draft, null); - - Assert.True(result); - } - - [Fact] - public async Task Should_be_able_to_update_published() - { - var content = new ContentEntity { Status = Status.Published }; - - var result = await sut.CanUpdateAsync(content); - - Assert.True(result); - } - - [Fact] - public async Task Should_be_able_to_update_draft() - { - var content = new ContentEntity { Status = Status.Published }; - - var result = await sut.CanUpdateAsync(content); - - Assert.True(result); - } - - [Fact] - public async Task Should_not_be_able_to_update_archived() - { - var content = new ContentEntity { Status = Status.Archived }; - - var result = await sut.CanUpdateAsync(content); - - Assert.False(result); - } - - [Fact] - public async Task Should_get_next_statuses_for_draft() - { - var content = new ContentEntity { Status = Status.Draft }; - - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Published, StatusColors.Published) - }; - - var result = await sut.GetNextsAsync(content, null); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_get_next_statuses_for_archived() - { - var content = new ContentEntity { Status = Status.Archived }; - - var expected = new[] - { - new StatusInfo(Status.Draft, StatusColors.Draft) - }; - - var result = await sut.GetNextsAsync(content, null); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_get_next_statuses_for_published() - { - var content = new ContentEntity { Status = Status.Published }; - - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft) - }; - - var result = await sut.GetNextsAsync(content, null); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_return_all_statuses() - { - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft), - new StatusInfo(Status.Published, StatusColors.Published) - }; - - var result = await sut.GetAllAsync(null); - - result.Should().BeEquivalentTo(expected); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs deleted file mode 100644 index 2159344b4..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -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.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class DefaultWorkflowsValidatorTests - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly DefaultWorkflowsValidator sut; - - public DefaultWorkflowsValidatorTests() - { - var schema = Mocks.Schema(appId, schemaId, new Schema(schemaId.Name)); - - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) - .Returns(Task.FromResult(null)); - - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) - .Returns(schema); - - sut = new DefaultWorkflowsValidator(appProvider); - } - - [Fact] - public async Task Should_generate_error_if_multiple_workflows_cover_all_schemas() - { - var workflows = Workflows.Empty - .Add(Guid.NewGuid(), "workflow1") - .Add(Guid.NewGuid(), "workflow2"); - - var errors = await sut.ValidateAsync(appId.Id, workflows); - - Assert.Equal(errors, new[] { "Multiple workflows cover all schemas." }); - } - - [Fact] - public async Task Should_generate_error_if_multiple_workflows_cover_specific_schema() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var workflows = Workflows.Empty - .Add(id1, "workflow1") - .Add(id2, "workflow2") - .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })) - .Update(id2, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })); - - var errors = await sut.ValidateAsync(appId.Id, workflows); - - Assert.Equal(errors, new[] { "The schema `my-schema` is covered by multiple workflows." }); - } - - [Fact] - public async Task Should_not_generate_error_if_schema_deleted() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var oldSchemaId = Guid.NewGuid(); - - var workflows = Workflows.Empty - .Add(id1, "workflow1") - .Add(id2, "workflow2") - .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { oldSchemaId })) - .Update(id2, new Workflow(default, Workflow.EmptySteps, new List { oldSchemaId })); - - var errors = await sut.ValidateAsync(appId.Id, workflows); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_generate_errors_for_no_overlaps() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var workflows = Workflows.Empty - .Add(id1, "workflow1") - .Add(id2, "workflow2") - .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })); - - var errors = await sut.ValidateAsync(appId.Id, workflows); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_generate_errors_for_empty_workflows() - { - var errors = await sut.ValidateAsync(appId.Id, Workflows.Empty); - - Assert.Empty(errors); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs deleted file mode 100644 index 73eb10962..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs +++ /dev/null @@ -1,352 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class DynamicContentWorkflowTests - { - private readonly IAppEntity app; - private readonly IAppProvider appProvider = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly NamedId simpleSchemaId = NamedId.Of(Guid.NewGuid(), "my-simple-schema"); - private readonly DynamicContentWorkflow sut; - - private readonly Workflow workflow = new Workflow( - Status.Draft, - new Dictionary - { - [Status.Archived] = - new WorkflowStep( - new Dictionary - { - [Status.Draft] = new WorkflowTransition() - }, - StatusColors.Archived, true), - [Status.Draft] = - new WorkflowStep( - new Dictionary - { - [Status.Archived] = new WorkflowTransition(), - [Status.Published] = new WorkflowTransition("data.field.iv === 2", ReadOnlyCollection.Create("Owner", "Editor")) - }, - StatusColors.Draft), - [Status.Published] = - new WorkflowStep( - new Dictionary - { - [Status.Archived] = new WorkflowTransition(), - [Status.Draft] = new WorkflowTransition() - }, - StatusColors.Published) - }); - - public DynamicContentWorkflowTests() - { - app = Mocks.App(appId); - - var simpleWorkflow = new Workflow( - Status.Draft, - new Dictionary - { - [Status.Draft] = - new WorkflowStep( - new Dictionary - { - [Status.Published] = new WorkflowTransition() - }, - StatusColors.Draft), - [Status.Published] = - new WorkflowStep( - new Dictionary - { - [Status.Draft] = new WorkflowTransition() - }, - StatusColors.Published) - }, - new List { simpleSchemaId.Id }); - - var workflows = Workflows.Empty.Set(workflow).Set(Guid.NewGuid(), simpleWorkflow); - - A.CallTo(() => appProvider.GetAppAsync(appId.Id)) - .Returns(app); - - A.CallTo(() => app.Workflows) - .Returns(workflows); - - sut = new DynamicContentWorkflow(new JintScriptEngine(), appProvider); - } - - [Fact] - public async Task Should_draft_as_initial_status() - { - var expected = new StatusInfo(Status.Draft, StatusColors.Draft); - - var result = await sut.GetInitialStatusAsync(Mocks.Schema(appId, schemaId)); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_allow_publish_on_create() - { - var content = CreateContent(Status.Draft, 2); - - var result = await sut.CanPublishOnCreateAsync(Mocks.Schema(appId, schemaId), content.DataDraft, Mocks.FrontendUser("Editor")); - - Assert.True(result); - } - - [Fact] - public async Task Should_not_allow_publish_on_create_if_data_is_invalid() - { - var content = CreateContent(Status.Draft, 4); - - var result = await sut.CanPublishOnCreateAsync(Mocks.Schema(appId, schemaId), content.DataDraft, Mocks.FrontendUser("Editor")); - - Assert.False(result); - } - - [Fact] - public async Task Should_not_allow_publish_on_create_if_role_not_allowed() - { - var content = CreateContent(Status.Draft, 2); - - var result = await sut.CanPublishOnCreateAsync(Mocks.Schema(appId, schemaId), content.DataDraft, Mocks.FrontendUser("Developer")); - - Assert.False(result); - } - - [Fact] - public async Task Should_check_is_valid_next() - { - var content = CreateContent(Status.Draft, 2); - - var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Editor")); - - Assert.True(result); - } - - [Fact] - public async Task Should_not_allow_transition_if_role_is_not_allowed() - { - var content = CreateContent(Status.Draft, 2); - - var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Developer")); - - Assert.False(result); - } - - [Fact] - public async Task Should_allow_transition_if_role_is_allowed() - { - var content = CreateContent(Status.Draft, 2); - - var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Editor")); - - Assert.True(result); - } - - [Fact] - public async Task Should_not_allow_transition_if_data_not_valid() - { - var content = CreateContent(Status.Draft, 4); - - var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Editor")); - - Assert.False(result); - } - - [Fact] - public async Task Should_be_able_to_update_published() - { - var content = CreateContent(Status.Published, 2); - - var result = await sut.CanUpdateAsync(content); - - Assert.True(result); - } - - [Fact] - public async Task Should_be_able_to_update_draft() - { - var content = CreateContent(Status.Published, 2); - - var result = await sut.CanUpdateAsync(content); - - Assert.True(result); - } - - [Fact] - public async Task Should_not_be_able_to_update_archived() - { - var content = CreateContent(Status.Archived, 2); - - var result = await sut.CanUpdateAsync(content); - - Assert.False(result); - } - - [Fact] - public async Task Should_get_next_statuses_for_draft() - { - var content = CreateContent(Status.Draft, 2); - - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived) - }; - - var result = await sut.GetNextsAsync(content, Mocks.FrontendUser("Developer")); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_limit_next_statuses_if_expression_does_not_evauate_to_true() - { - var content = CreateContent(Status.Draft, 4); - - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived) - }; - - var result = await sut.GetNextsAsync(content, Mocks.FrontendUser("Editor")); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_limit_next_statuses_if_role_is_not_allowed() - { - var content = CreateContent(Status.Draft, 2); - - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Published, StatusColors.Published) - }; - - var result = await sut.GetNextsAsync(content, Mocks.FrontendUser("Editor")); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_get_next_statuses_for_archived() - { - var content = CreateContent(Status.Archived, 2); - - var expected = new[] - { - new StatusInfo(Status.Draft, StatusColors.Draft) - }; - - var result = await sut.GetNextsAsync(content, null); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_get_next_statuses_for_published() - { - var content = CreateContent(Status.Published, 2); - - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft) - }; - - var result = await sut.GetNextsAsync(content, null); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_return_all_statuses() - { - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft), - new StatusInfo(Status.Published, StatusColors.Published) - }; - - var result = await sut.GetAllAsync(Mocks.Schema(appId, schemaId)); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_return_all_statuses_for_simple_schema_workflow() - { - var expected = new[] - { - new StatusInfo(Status.Draft, StatusColors.Draft), - new StatusInfo(Status.Published, StatusColors.Published) - }; - - var result = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_return_all_statuses_for_default_workflow_when_no_workflow_configured() - { - A.CallTo(() => app.Workflows).Returns(Workflows.Empty); - - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft), - new StatusInfo(Status.Published, StatusColors.Published) - }; - - var result = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); - - result.Should().BeEquivalentTo(expected); - } - - private IContentEntity CreateContent(Status status, int value, bool simple = false) - { - var content = new ContentEntity { AppId = appId, Status = status }; - - if (simple) - { - content.SchemaId = simpleSchemaId; - } - else - { - content.SchemaId = schemaId; - } - - content.DataDraft = - new NamedContentData() - .AddField("field", - new ContentFieldData() - .AddValue("iv", value)); - - return content; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs deleted file mode 100644 index 78c2ad0c8..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ /dev/null @@ -1,1270 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public class GraphQLQueriesTests : GraphQLTestBase - { - [Fact] - public async Task Should_introspect() - { - const string query = @" - query IntrospectionQuery { - __schema { - queryType { name } - mutationType { name } - subscriptionType { name } - types { - ...FullType - } - directives { - name - description - args { - ...InputValue - } - onOperation - onFragment - onField - } - } - } - - fragment FullType on __Type { - kind - name - description - fields(includeDeprecated: true) { - name - description - args { - ...InputValue - } - type { - ...TypeRef - } - isDeprecated - deprecationReason - } - inputFields { - ...InputValue - } - interfaces { - ...TypeRef - } - enumValues(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason - } - possibleTypes { - ...TypeRef - } - } - - fragment InputValue on __InputValue { - name - description - type { ...TypeRef } - defaultValue - } - - fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - } - } - } - }"; - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, OperationName = "IntrospectionQuery" }); - - var json = serializer.Serialize(result.Response, true); - - Assert.NotEmpty(json); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public async Task Should_return_empty_object_for_empty_query(string query) - { - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_multiple_assets_when_querying_assets() - { - const string query = @" - query { - queryAssets(filter: ""my-query"", top: 30, skip: 5) { - id - version - created - createdBy - lastModified - lastModifiedBy - url - thumbnailUrl - sourceUrl - mimeType - fileName - fileHash - fileSize - fileVersion - isImage - pixelWidth - pixelHeight - tags - slug - } - }"; - - var asset = CreateAsset(Guid.NewGuid()); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query"))) - .Returns(ResultList.CreateFrom(0, asset)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - queryAssets = new dynamic[] - { - new - { - id = asset.Id, - version = 1, - created = asset.Created, - createdBy = "subject:user1", - lastModified = asset.LastModified, - lastModifiedBy = "subject:user2", - url = $"assets/{asset.Id}", - thumbnailUrl = $"assets/{asset.Id}?width=100", - sourceUrl = $"assets/source/{asset.Id}", - mimeType = "image/png", - fileName = "MyFile.png", - fileHash = "ABC123", - fileSize = 1024, - fileVersion = 123, - isImage = true, - pixelWidth = 800, - pixelHeight = 600, - tags = new[] { "tag1", "tag2" }, - slug = "myfile.png" - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_multiple_assets_with_total_when_querying_assets_with_total() - { - const string query = @" - query { - queryAssetsWithTotal(filter: ""my-query"", top: 30, skip: 5) { - total - items { - id - version - created - createdBy - lastModified - lastModifiedBy - url - thumbnailUrl - sourceUrl - mimeType - fileName - fileHash - fileSize - fileVersion - isImage - pixelWidth - pixelHeight - tags - slug - } - } - }"; - - var asset = CreateAsset(Guid.NewGuid()); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query"))) - .Returns(ResultList.CreateFrom(10, asset)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - queryAssetsWithTotal = new - { - total = 10, - items = new dynamic[] - { - new - { - id = asset.Id, - version = 1, - created = asset.Created, - createdBy = "subject:user1", - lastModified = asset.LastModified, - lastModifiedBy = "subject:user2", - url = $"assets/{asset.Id}", - thumbnailUrl = $"assets/{asset.Id}?width=100", - sourceUrl = $"assets/source/{asset.Id}", - mimeType = "image/png", - fileName = "MyFile.png", - fileHash = "ABC123", - fileSize = 1024, - fileVersion = 123, - isImage = true, - pixelWidth = 800, - pixelHeight = 600, - tags = new[] { "tag1", "tag2" }, - slug = "myfile.png" - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_null_single_asset() - { - var assetId = Guid.NewGuid(); - - var query = @" - query { - findAsset(id: """") { - id - } - }".Replace("", assetId.ToString()); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId))) - .Returns(ResultList.CreateFrom(1)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findAsset = (object)null - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_single_asset_when_finding_asset() - { - var assetId = Guid.NewGuid(); - var asset = CreateAsset(assetId); - - var query = @" - query { - findAsset(id: """") { - id - version - created - createdBy - lastModified - lastModifiedBy - url - thumbnailUrl - sourceUrl - mimeType - fileName - fileHash - fileSize - fileVersion - isImage - pixelWidth - pixelHeight - tags - slug - } - }".Replace("", assetId.ToString()); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId))) - .Returns(ResultList.CreateFrom(1, asset)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findAsset = new - { - id = asset.Id, - version = 1, - created = asset.Created, - createdBy = "subject:user1", - lastModified = asset.LastModified, - lastModifiedBy = "subject:user2", - url = $"assets/{asset.Id}", - thumbnailUrl = $"assets/{asset.Id}?width=100", - sourceUrl = $"assets/source/{asset.Id}", - mimeType = "image/png", - fileName = "MyFile.png", - fileHash = "ABC123", - fileSize = 1024, - fileVersion = 123, - isImage = true, - pixelWidth = 800, - pixelHeight = 600, - tags = new[] { "tag1", "tag2" }, - slug = "myfile.png" - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_multiple_contents_when_querying_contents() - { - const string query = @" - query { - queryMySchemaContents(top: 30, skip: 5) { - id - version - created - createdBy - lastModified - lastModifiedBy - status - statusColor - url - data { - myString { - de - } - myNumber { - iv - } - myBoolean { - iv - } - myDatetime { - iv - } - myJson { - iv - } - myGeolocation { - iv - } - myTags { - iv - } - myLocalized { - de_DE - } - myArray { - iv { - nestedNumber - nestedBoolean - } - } - } - } - }"; - - var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) - .Returns(ResultList.CreateFrom(0, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - queryMySchemaContents = new dynamic[] - { - new - { - id = content.Id, - version = 1, - created = content.Created, - createdBy = "subject:user1", - lastModified = content.LastModified, - lastModifiedBy = "subject:user2", - status = "DRAFT", - statusColor = "red", - url = $"contents/my-schema/{content.Id}", - data = new - { - myString = new - { - de = "value" - }, - myNumber = new - { - iv = 1 - }, - myBoolean = new - { - iv = true - }, - myDatetime = new - { - iv = content.LastModified - }, - myJson = new - { - iv = new - { - value = 1 - } - }, - myGeolocation = new - { - iv = new - { - latitude = 10, - longitude = 20 - } - }, - myTags = new - { - iv = new[] - { - "tag1", - "tag2" - } - }, - myLocalized = new - { - de_DE = "de-DE" - }, - myArray = new - { - iv = new[] - { - new - { - nestedNumber = 10, - nestedBoolean = true - }, - new - { - nestedNumber = 20, - nestedBoolean = false - } - } - } - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_multiple_contents_with_total_when_querying_contents_with_total() - { - const string query = @" - query { - queryMySchemaContentsWithTotal(top: 30, skip: 5) { - total - items { - id - version - created - createdBy - lastModified - lastModifiedBy - status - statusColor - url - data { - myString { - de - } - myNumber { - iv - } - myBoolean { - iv - } - myDatetime { - iv - } - myJson { - iv - } - myGeolocation { - iv - } - myTags { - iv - } - myLocalized { - de_DE - } - } - } - } - }"; - - var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) - .Returns(ResultList.CreateFrom(10, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - queryMySchemaContentsWithTotal = new - { - total = 10, - items = new dynamic[] - { - new - { - id = content.Id, - version = 1, - created = content.Created, - createdBy = "subject:user1", - lastModified = content.LastModified, - lastModifiedBy = "subject:user2", - status = "DRAFT", - statusColor = "red", - url = $"contents/my-schema/{content.Id}", - data = new - { - myString = new - { - de = "value" - }, - myNumber = new - { - iv = 1 - }, - myBoolean = new - { - iv = true - }, - myDatetime = new - { - iv = content.LastModified - }, - myJson = new - { - iv = new - { - value = 1 - } - }, - myGeolocation = new - { - iv = new - { - latitude = 10, - longitude = 20 - } - }, - myTags = new - { - iv = new[] - { - "tag1", - "tag2" - } - }, - myLocalized = new - { - de_DE = "de-DE" - } - } - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_single_content_with_duplicate_names() - { - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, Guid.Empty); - - var query = @" - query { - findMySchemaContent(id: """") { - data { - myNumber { - iv - } - myNumber2 { - iv - } - myArray { - iv { - nestedNumber - nestedNumber2 - } - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - data = new - { - myNumber = new - { - iv = 1 - }, - myNumber2 = new - { - iv = 2 - }, - myArray = new - { - iv = new[] - { - new - { - nestedNumber = 10, - nestedNumber2 = 11 - }, - new - { - nestedNumber = 20, - nestedNumber2 = 21 - } - } - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_null_single_content() - { - var contentId = Guid.NewGuid(); - - var query = @" - query { - findMySchemaContent(id: """") { - id - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = (object)null - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_single_content_when_finding_content() - { - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, Guid.Empty); - - var query = @" - query { - findMySchemaContent(id: """") { - id - version - created - createdBy - lastModified - lastModifiedBy - status - statusColor - url - data { - myString { - de - } - myNumber { - iv - } - myBoolean { - iv - } - myDatetime { - iv - } - myJson { - iv - } - myGeolocation { - iv - } - myTags { - iv - } - myLocalized { - de_DE - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - id = content.Id, - version = 1, - created = content.Created, - createdBy = "subject:user1", - lastModified = content.LastModified, - lastModifiedBy = "subject:user2", - status = "DRAFT", - statusColor = "red", - url = $"contents/my-schema/{content.Id}", - data = new - { - myString = new - { - de = "value" - }, - myNumber = new - { - iv = 1 - }, - myBoolean = new - { - iv = true - }, - myDatetime = new - { - iv = content.LastModified - }, - myJson = new - { - iv = new - { - value = 1 - } - }, - myGeolocation = new - { - iv = new - { - latitude = 10, - longitude = 20 - } - }, - myTags = new - { - iv = new[] - { - "tag1", - "tag2" - } - }, - myLocalized = new - { - de_DE = "de-DE" - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_also_fetch_referenced_contents_when_field_is_included_in_query() - { - var contentRefId = Guid.NewGuid(); - var contentRef = CreateRefContent(schemaRefId1, contentRefId, "ref1-field", "ref1"); - - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, contentRefId, Guid.Empty); - - var query = @" - query { - findMySchemaContent(id: """") { - id - data { - myReferences { - iv { - id - data { - ref1Field { - iv - } - } - } - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>.Ignored)) - .Returns(ResultList.CreateFrom(0, contentRef)); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - id = content.Id, - data = new - { - myReferences = new - { - iv = new[] - { - new - { - id = contentRefId, - data = new - { - ref1Field = new - { - iv = "ref1" - } - } - } - } - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_also_fetch_union_contents_when_field_is_included_in_query() - { - var contentRefId = Guid.NewGuid(); - var contentRef = CreateRefContent(schemaRefId1, contentRefId, "ref1-field", "ref1"); - - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, contentRefId, Guid.Empty); - - var query = @" - query { - findMySchemaContent(id: """") { - id - data { - myUnion { - iv { - ... on Content { - id - } - ... on MyRefSchema1 { - data { - ref1Field { - iv - } - } - } - __typename - } - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>.Ignored)) - .Returns(ResultList.CreateFrom(0, contentRef)); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - id = content.Id, - data = new - { - myUnion = new - { - iv = new[] - { - new - { - id = contentRefId, - data = new - { - ref1Field = new - { - iv = "ref1" - } - }, - __typename = "MyRefSchema1" - } - } - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_also_fetch_referenced_assets_when_field_is_included_in_query() - { - var assetRefId = Guid.NewGuid(); - var assetRef = CreateAsset(assetRefId); - - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, assetRefId); - - var query = @" - query { - findMySchemaContent(id: """") { - id - data { - myAssets { - iv { - id - } - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.Ignored)) - .Returns(ResultList.CreateFrom(0, assetRef)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - id = content.Id, - data = new - { - myAssets = new - { - iv = new[] - { - new - { - id = assetRefId - } - } - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_make_multiple_queries() - { - var assetId1 = Guid.NewGuid(); - var assetId2 = Guid.NewGuid(); - var asset1 = CreateAsset(assetId1); - var asset2 = CreateAsset(assetId2); - - var query1 = @" - query { - findAsset(id: """") { - id - } - }".Replace("", assetId1.ToString()); - var query2 = @" - query { - findAsset(id: """") { - id - } - }".Replace("", assetId2.ToString()); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId1))) - .Returns(ResultList.CreateFrom(0, asset1)); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId2))) - .Returns(ResultList.CreateFrom(0, asset2)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query1 }, new GraphQLQuery { Query = query2 }); - - var expected = new object[] - { - new - { - data = new - { - findAsset = new - { - id = asset1.Id - } - } - }, - new - { - data = new - { - findAsset = new - { - id = asset2.Id - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_not_return_data_when_field_not_part_of_content() - { - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, Guid.Empty, new NamedContentData()); - - var query = @" - query { - findMySchemaContent(id: """") { - id - version - created - createdBy - lastModified - lastModifiedBy - url - data { - myInvalid { - iv - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var json = serializer.Serialize(result); - - Assert.Contains("\"data\":null", json); - } - - [Fact] - public async Task Should_return_draft_content_when_querying_dataDraft() - { - var dataDraft = new NamedContentData() - .AddField("my-string", - new ContentFieldData() - .AddValue("de", "draft value")) - .AddField("my-number", - new ContentFieldData() - .AddValue("iv", 42)); - - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, Guid.Empty, null, dataDraft); - - var query = @" - query { - findMySchemaContent(id: """") { - dataDraft { - myString { - de - } - myNumber { - iv - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - dataDraft = new - { - myString = new - { - de = "draft value" - }, - myNumber = new - { - iv = 42 - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_null_when_querying_dataDraft_and_no_draft_content_is_available() - { - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, Guid.Empty, null); - - var query = @" - query { - findMySchemaContent(id: """") { - dataDraft { - myString { - de - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - dataDraft = (object)null - } - } - }; - - AssertResult(expected, result); - } - - private static IReadOnlyList MatchId(Guid contentId) - { - return A>.That.Matches(x => x.Count == 1 && x[0] == contentId); - } - - private static Q MatchIdQuery(Guid contentId) - { - return A.That.Matches(x => x.Ids.Count == 1 && x.Ids[0] == contentId); - } - - private Context MatchsAssetContext() - { - return A.That.Matches(x => x.App == app && x.User == requestContext.User); - } - - private Context MatchsContentContext() - { - return A.That.Matches(x => x.App == app && x.User == requestContext.User); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs deleted file mode 100644 index e5eb10df8..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ /dev/null @@ -1,286 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using FakeItEasy; -using GraphQL; -using GraphQL.DataLoader; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using NodaTime; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents.TestData; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Log; -using Xunit; - -#pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter -#pragma warning disable SA1401 // Fields must be private - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public class GraphQLTestBase - { - protected readonly IAppEntity app; - protected readonly IAssetQueryService assetQuery = A.Fake(); - protected readonly IContentQueryService contentQuery = A.Fake(); - protected readonly IJsonSerializer serializer = TestUtils.CreateSerializer(TypeNameHandling.None); - protected readonly ISchemaEntity schema; - protected readonly ISchemaEntity schemaRef1; - protected readonly ISchemaEntity schemaRef2; - protected readonly Context requestContext; - protected readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - protected readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - protected readonly NamedId schemaRefId1 = NamedId.Of(Guid.NewGuid(), "my-ref-schema1"); - protected readonly NamedId schemaRefId2 = NamedId.Of(Guid.NewGuid(), "my-ref-schema2"); - protected readonly IGraphQLService sut; - - public GraphQLTestBase() - { - app = Mocks.App(appId, Language.DE, Language.GermanGermany); - - var schemaDef = - new Schema(schemaId.Name) - .Publish() - .AddJson(1, "my-json", Partitioning.Invariant, - new JsonFieldProperties()) - .AddString(2, "my-string", Partitioning.Language, - new StringFieldProperties()) - .AddNumber(3, "my-number", Partitioning.Invariant, - new NumberFieldProperties()) - .AddNumber(4, "my_number", Partitioning.Invariant, - new NumberFieldProperties()) - .AddAssets(5, "my-assets", Partitioning.Invariant, - new AssetsFieldProperties()) - .AddBoolean(6, "my-boolean", Partitioning.Invariant, - new BooleanFieldProperties()) - .AddDateTime(7, "my-datetime", Partitioning.Invariant, - new DateTimeFieldProperties()) - .AddReferences(8, "my-references", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaRefId1.Id }) - .AddReferences(81, "my-union", Partitioning.Invariant, - new ReferencesFieldProperties()) - .AddReferences(9, "my-invalid", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = Guid.NewGuid() }) - .AddGeolocation(10, "my-geolocation", Partitioning.Invariant, - new GeolocationFieldProperties()) - .AddTags(11, "my-tags", Partitioning.Invariant, - new TagsFieldProperties()) - .AddString(12, "my-localized", Partitioning.Language, - new StringFieldProperties()) - .AddArray(13, "my-array", Partitioning.Invariant, f => f - .AddBoolean(121, "nested-boolean") - .AddNumber(122, "nested-number") - .AddNumber(123, "nested_number")) - .ConfigureScripts(new SchemaScripts { Query = "" }); - - schema = Mocks.Schema(appId, schemaId, schemaDef); - - var schemaRef1Def = - new Schema(schemaRefId1.Name) - .Publish() - .AddString(1, "ref1-field", Partitioning.Invariant); - - schemaRef1 = Mocks.Schema(appId, schemaRefId1, schemaRef1Def); - - var schemaRef2Def = - new Schema(schemaRefId2.Name) - .Publish() - .AddString(1, "ref2-field", Partitioning.Invariant); - - schemaRef2 = Mocks.Schema(appId, schemaRefId2, schemaRef2Def); - - requestContext = new Context(Mocks.FrontendUser(), app); - - sut = CreateSut(); - } - - protected IEnrichedContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null, NamedContentData dataDraft = null) - { - var now = SystemClock.Instance.GetCurrentInstant(); - - data = data ?? - new NamedContentData() - .AddField("my-string", - new ContentFieldData() - .AddValue("de", "value")) - .AddField("my-assets", - new ContentFieldData() - .AddValue("iv", JsonValue.Array(assetId.ToString()))) - .AddField("my-number", - new ContentFieldData() - .AddValue("iv", 1.0)) - .AddField("my_number", - new ContentFieldData() - .AddValue("iv", 2.0)) - .AddField("my-boolean", - new ContentFieldData() - .AddValue("iv", true)) - .AddField("my-datetime", - new ContentFieldData() - .AddValue("iv", now)) - .AddField("my-tags", - new ContentFieldData() - .AddValue("iv", JsonValue.Array("tag1", "tag2"))) - .AddField("my-references", - new ContentFieldData() - .AddValue("iv", JsonValue.Array(refId.ToString()))) - .AddField("my-union", - new ContentFieldData() - .AddValue("iv", JsonValue.Array(refId.ToString()))) - .AddField("my-geolocation", - new ContentFieldData() - .AddValue("iv", JsonValue.Object().Add("latitude", 10).Add("longitude", 20))) - .AddField("my-json", - new ContentFieldData() - .AddValue("iv", JsonValue.Object().Add("value", 1))) - .AddField("my-localized", - new ContentFieldData() - .AddValue("de-DE", "de-DE")) - .AddField("my-array", - new ContentFieldData() - .AddValue("iv", JsonValue.Array( - JsonValue.Object() - .Add("nested-boolean", true) - .Add("nested-number", 10) - .Add("nested_number", 11), - JsonValue.Object() - .Add("nested-boolean", false) - .Add("nested-number", 20) - .Add("nested_number", 21)))); - - var content = new ContentEntity - { - Id = id, - Version = 1, - Created = now, - CreatedBy = new RefToken(RefTokenType.Subject, "user1"), - LastModified = now, - LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), - Data = data, - DataDraft = dataDraft, - SchemaId = schemaId, - Status = Status.Draft, - StatusColor = "red" - }; - - return content; - } - - protected static IEnrichedContentEntity CreateRefContent(NamedId schemaId, Guid id, string field, string value) - { - var now = SystemClock.Instance.GetCurrentInstant(); - - var data = - new NamedContentData() - .AddField(field, - new ContentFieldData() - .AddValue("iv", value)); - - var content = new ContentEntity - { - Id = id, - Version = 1, - Created = now, - CreatedBy = new RefToken(RefTokenType.Subject, "user1"), - LastModified = now, - LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), - Data = data, - DataDraft = data, - SchemaId = schemaId, - Status = Status.Draft, - StatusColor = "red" - }; - - return content; - } - - protected static IEnrichedAssetEntity CreateAsset(Guid id) - { - var now = SystemClock.Instance.GetCurrentInstant(); - - var asset = new AssetEntity - { - Id = id, - Version = 1, - Created = now, - CreatedBy = new RefToken(RefTokenType.Subject, "user1"), - LastModified = now, - LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), - FileName = "MyFile.png", - Slug = "myfile.png", - FileSize = 1024, - FileHash = "ABC123", - FileVersion = 123, - MimeType = "image/png", - IsImage = true, - PixelWidth = 800, - PixelHeight = 600, - TagNames = new[] { "tag1", "tag2" }.ToHashSet() - }; - - return asset; - } - - protected void AssertResult(object expected, (bool HasErrors, object Response) result, bool checkErrors = true) - { - if (checkErrors && result.HasErrors) - { - throw new InvalidOperationException(Serialize(result)); - } - - var resultJson = serializer.Serialize(result.Response, true); - var expectJson = serializer.Serialize(expected, true); - - Assert.Equal(expectJson, resultJson); - } - - private string Serialize((bool HasErrors, object Response) result) - { - return serializer.Serialize(result); - } - - private CachingGraphQLService CreateSut() - { - var appProvider = A.Fake(); - - A.CallTo(() => appProvider.GetSchemasAsync(appId.Id)) - .Returns(new List { schema, schemaRef1, schemaRef2 }); - - var dataLoaderContext = new DataLoaderContextAccessor(); - - var services = new Dictionary - { - [typeof(IAppProvider)] = appProvider, - [typeof(IAssetQueryService)] = assetQuery, - [typeof(IContentQueryService)] = contentQuery, - [typeof(IDataLoaderContextAccessor)] = dataLoaderContext, - [typeof(IGraphQLUrlGenerator)] = new FakeUrlGenerator(), - [typeof(IOptions)] = Options.Create(new AssetOptions()), - [typeof(IOptions)] = Options.Create(new ContentOptions()), - [typeof(ISemanticLog)] = A.Fake(), - [typeof(DataLoaderDocumentListener)] = new DataLoaderDocumentListener(dataLoaderContext) - }; - - var resolver = new FuncDependencyResolver(t => services[t]); - - var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - - return new CachingGraphQLService(cache, resolver); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs deleted file mode 100644 index 76b23f2eb..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs +++ /dev/null @@ -1,289 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using FakeItEasy; -using MongoDB.Bson.Serialization; -using MongoDB.Driver; -using NodaTime.Text; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.MongoDb.Contents; -using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.MongoDb.Queries; -using Squidex.Infrastructure.Queries; -using Xunit; -using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; -using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; - -namespace Squidex.Domain.Apps.Entities.Contents.MongoDb -{ - public class MongoDbQueryTests - { - private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; - private static readonly IBsonSerializer Serializer = BsonSerializer.SerializerRegistry.GetSerializer(); - private readonly Schema schemaDef; - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); - - static MongoDbQueryTests() - { - InstantSerializer.Register(); - } - - public MongoDbQueryTests() - { - schemaDef = - new Schema("user") - .AddString(1, "firstName", Partitioning.Language, - new StringFieldProperties()) - .AddString(2, "lastName", Partitioning.Language, - new StringFieldProperties()) - .AddBoolean(3, "isAdmin", Partitioning.Invariant, - new BooleanFieldProperties()) - .AddNumber(4, "age", Partitioning.Invariant, - new NumberFieldProperties()) - .AddDateTime(5, "birthday", Partitioning.Invariant, - new DateTimeFieldProperties()) - .AddAssets(6, "pictures", Partitioning.Invariant, - new AssetsFieldProperties()) - .AddReferences(7, "friends", Partitioning.Invariant, - new ReferencesFieldProperties()) - .AddString(8, "dashed-field", Partitioning.Invariant, - new StringFieldProperties()) - .AddArray(9, "hobbies", Partitioning.Invariant, a => a - .AddString(91, "name")) - .Update(new SchemaProperties()); - - var schema = A.Dummy(); - A.CallTo(() => schema.Id).Returns(Guid.NewGuid()); - A.CallTo(() => schema.Version).Returns(3); - A.CallTo(() => schema.SchemaDef).Returns(schemaDef); - - var app = A.Dummy(); - A.CallTo(() => app.Id).Returns(Guid.NewGuid()); - A.CallTo(() => app.Version).Returns(3); - A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); - } - - [Fact] - public void Should_throw_exception_for_invalid_field() - { - Assert.Throws(() => F(ClrFilter.Eq("data/invalid/iv", "Me"))); - } - - [Fact] - public void Should_make_query_with_lastModified() - { - var i = F(ClrFilter.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - var o = C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_lastModifiedBy() - { - var i = F(ClrFilter.Eq("lastModifiedBy", "Me")); - var o = C("{ 'mb' : 'Me' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_created() - { - var i = F(ClrFilter.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - var o = C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_createdBy() - { - var i = F(ClrFilter.Eq("createdBy", "Me")); - var o = C("{ 'cb' : 'Me' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_version() - { - var i = F(ClrFilter.Eq("version", 0L)); - var o = C("{ 'vs' : NumberLong(0) }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_version_and_list() - { - var i = F(ClrFilter.In("version", new List { 0L, 2L, 5L })); - var o = C("{ 'vs' : { '$in' : [NumberLong(0), NumberLong(2), NumberLong(5)] } }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_from_draft() - { - var i = F(ClrFilter.Eq("data/dashed_field/iv", "Value"), true); - var o = C("{ 'dd.8.iv' : 'Value' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_empty_test() - { - var i = F(ClrFilter.Empty("data/firstName/iv"), true); - var o = C("{ '$or' : [{ 'dd.1.iv' : { '$exists' : false } }, { 'dd.1.iv' : null }, { 'dd.1.iv' : '' }, { 'dd.1.iv' : [] }] }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_datetime_data() - { - var i = F(ClrFilter.Eq("data/birthday/iv", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_underscore_field() - { - var i = F(ClrFilter.Eq("data/dashed_field/iv", "Value")); - var o = C("{ 'do.8.iv' : 'Value' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_references_equals() - { - var i = F(ClrFilter.Eq("data/friends/iv", "guid")); - var o = C("{ 'do.7.iv' : 'guid' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_array_field() - { - var i = F(ClrFilter.Eq("data/hobbies/iv/name", "PC")); - var o = C("{ 'do.9.iv.91' : 'PC' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_assets_equals() - { - var i = F(ClrFilter.Eq("data/pictures/iv", "guid")); - var o = C("{ 'do.6.iv' : 'guid' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_full_text() - { - var i = Q(new ClrQuery { FullText = "Hello my World" }); - var o = C("{ '$text' : { '$search' : 'Hello my World' } }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_orderby_with_single_field() - { - var i = S(SortBuilder.Descending("data/age/iv")); - var o = C("{ 'do.4.iv' : -1 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_orderby_with_multiple_fields() - { - var i = S(SortBuilder.Ascending("data/age/iv"), SortBuilder.Descending("data/firstName/en")); - var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_take_statement() - { - var query = new ClrQuery { Take = 3 }; - var cursor = A.Fake>(); - - cursor.ContentTake(query.AdjustToModel(schemaDef, false)); - - A.CallTo(() => cursor.Limit(3)) - .MustHaveHappened(); - } - - [Fact] - public void Should_make_skip_statement() - { - var query = new ClrQuery { Skip = 3 }; - var cursor = A.Fake>(); - - cursor.ContentSkip(query.AdjustToModel(schemaDef, false)); - - A.CallTo(() => cursor.Skip(3)) - .MustHaveHappened(); - } - - private static string C(string value) - { - return value.Replace('\'', '"'); - } - - private string F(FilterNode filter, bool useDraft = false) - { - return Q(new ClrQuery { Filter = filter }, useDraft); - } - - private string S(params SortNode[] sorts) - { - var cursor = A.Fake>(); - - var i = string.Empty; - - A.CallTo(() => cursor.Sort(A>.Ignored)) - .Invokes((SortDefinition sortDefinition) => - { - i = sortDefinition.Render(Serializer, Registry).ToString(); - }); - - cursor.ContentSort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel(schemaDef, false)); - - return i; - } - - private string Q(ClrQuery query, bool useDraft = false) - { - var rendered = - query.AdjustToModel(schemaDef, useDraft).BuildFilter().Filter - .Render(Serializer, Registry).ToString(); - - return rendered; - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs deleted file mode 100644 index b1cae86c6..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs +++ /dev/null @@ -1,204 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public class ContentEnricherTests - { - private readonly IContentWorkflow contentWorkflow = A.Fake(); - private readonly IContentQueryService contentQuery = A.Fake(); - private readonly IAssetQueryService assetQuery = A.Fake(); - private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake(); - private readonly ISchemaEntity schema; - private readonly Context requestContext; - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly ContentEnricher sut; - - public ContentEnricherTests() - { - requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); - - schema = Mocks.Schema(appId, schemaId); - - A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(A.Ignored, schemaId.Id.ToString())) - .Returns(schema); - - sut = new ContentEnricher(assetQuery, assetUrlGenerator, new Lazy(() => contentQuery), contentWorkflow); - } - - [Fact] - public async Task Should_add_app_version_and_schema_as_dependency() - { - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Contains(requestContext.App.Version, result.CacheDependencies); - - Assert.Contains(schema.Id, result.CacheDependencies); - Assert.Contains(schema.Version, result.CacheDependencies); - } - - [Fact] - public async Task Should_enrich_with_reference_fields() - { - var ctx = new Context(Mocks.FrontendUser(), requestContext.App); - - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, ctx); - - Assert.NotNull(result.ReferenceFields); - } - - [Fact] - public async Task Should_not_enrich_with_reference_fields_when_not_frontend() - { - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Null(result.ReferenceFields); - } - - [Fact] - public async Task Should_enrich_with_schema_names() - { - var ctx = new Context(Mocks.FrontendUser(), requestContext.App); - - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, ctx); - - Assert.Equal("my-schema", result.SchemaName); - Assert.Equal("my-schema", result.SchemaDisplayName); - } - - [Fact] - public async Task Should_not_enrich_with_schema_names_when_not_frontend() - { - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Null(result.SchemaName); - Assert.Null(result.SchemaDisplayName); - } - - [Fact] - public async Task Should_enrich_content_with_status_color() - { - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Equal(StatusColors.Published, result.StatusColor); - } - - [Fact] - public async Task Should_enrich_content_with_default_color_if_not_found() - { - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(Task.FromResult(null)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Equal(StatusColors.Draft, result.StatusColor); - } - - [Fact] - public async Task Should_enrich_content_with_can_update() - { - requestContext.WithResolveFlow(true); - - var source = new ContentEntity { SchemaId = schemaId }; - - A.CallTo(() => contentWorkflow.CanUpdateAsync(source)) - .Returns(true); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.True(result.CanUpdate); - } - - [Fact] - public async Task Should_not_enrich_content_with_can_update_if_disabled_in_context() - { - requestContext.WithResolveFlow(false); - - var source = new ContentEntity { SchemaId = schemaId }; - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.False(result.CanUpdate); - - A.CallTo(() => contentWorkflow.CanUpdateAsync(source)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_enrich_multiple_contents_and_cache_color() - { - var source1 = PublishedContent(); - var source2 = PublishedContent(); - - var source = new IContentEntity[] - { - source1, - source2 - }; - - A.CallTo(() => contentWorkflow.GetInfoAsync(source1)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Equal(StatusColors.Published, result[0].StatusColor); - Assert.Equal(StatusColors.Published, result[1].StatusColor); - - A.CallTo(() => contentWorkflow.GetInfoAsync(A.Ignored)) - .MustHaveHappenedOnceExactly(); - } - - private ContentEntity PublishedContent() - { - return new ContentEntity { Status = Status.Published, SchemaId = schemaId }; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs deleted file mode 100644 index 0f9c591a6..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public class ContentLoaderTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly IContentGrain grain = A.Fake(); - private readonly Guid id = Guid.NewGuid(); - private readonly ContentLoader sut; - - public ContentLoaderTests() - { - A.CallTo(() => grainFactory.GetGrain(id, null)) - .Returns(grain); - - sut = new ContentLoader(grainFactory); - } - - [Fact] - public async Task Should_throw_exception_if_no_state_returned() - { - A.CallTo(() => grain.GetStateAsync(10)) - .Returns(J.Of(null)); - - await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); - } - - [Fact] - public async Task Should_throw_exception_if_state_has_other_version() - { - var content = new ContentEntity { Version = 5 }; - - A.CallTo(() => grain.GetStateAsync(10)) - .Returns(J.Of(content)); - - await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); - } - - [Fact] - public async Task Should_not_throw_exception_if_state_has_other_version_than_any() - { - var content = new ContentEntity { Version = 5 }; - - A.CallTo(() => grain.GetStateAsync(EtagVersion.Any)) - .Returns(J.Of(content)); - - await sut.GetAsync(id, EtagVersion.Any); - } - - [Fact] - public async Task Should_return_content_from_state() - { - var content = new ContentEntity { Version = 10 }; - - A.CallTo(() => grain.GetStateAsync(10)) - .Returns(J.Of(content)); - - var result = await sut.GetAsync(id, 10); - - Assert.Same(content, result); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs deleted file mode 100644 index bc89098d3..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ /dev/null @@ -1,503 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Security; -using Squidex.Shared; -using Squidex.Shared.Identity; -using Xunit; - -#pragma warning disable SA1401 // Fields must be private - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public class ContentQueryServiceTests - { - private readonly IAppEntity app; - private readonly IAppProvider appProvider = A.Fake(); - private readonly IAssetUrlGenerator urlGenerator = A.Fake(); - private readonly IContentEnricher contentEnricher = A.Fake(); - private readonly IContentRepository contentRepository = A.Fake(); - private readonly IContentLoader contentVersionLoader = A.Fake(); - private readonly ISchemaEntity schema; - private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly Guid contentId = Guid.NewGuid(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly NamedContentData contentData = new NamedContentData(); - private readonly NamedContentData contentTransformed = new NamedContentData(); - private readonly ClaimsPrincipal user; - private readonly ClaimsIdentity identity = new ClaimsIdentity(); - private readonly Context requestContext; - private readonly ContentQueryParser queryParser = A.Fake(); - private readonly ContentQueryService sut; - - public static IEnumerable ApiStatusTests = new[] - { - new object[] { 0, new[] { Status.Published } }, - new object[] { 1, null } - }; - - public ContentQueryServiceTests() - { - user = new ClaimsPrincipal(identity); - - app = Mocks.App(appId); - - requestContext = new Context(user, app); - - var schemaDef = - new Schema(schemaId.Name) - .ConfigureScripts(new SchemaScripts { Query = "" }); - - schema = Mocks.Schema(appId, schemaId, schemaDef); - - SetupEnricher(); - - A.CallTo(() => queryParser.ParseQuery(requestContext, schema, A.Ignored)) - .Returns(new ClrQuery()); - - sut = new ContentQueryService( - appProvider, - urlGenerator, - contentEnricher, - contentRepository, - contentVersionLoader, - scriptEngine, - queryParser); - } - - [Fact] - public async Task Should_return_schema_from_id_if_string_is_guid() - { - SetupSchemaFound(); - - var result = await sut.GetSchemaOrThrowAsync(requestContext, schemaId.Id.ToString()); - - Assert.Equal(schema, result); - } - - [Fact] - public async Task Should_return_schema_from_name_if_string_not_guid() - { - SetupSchemaFound(); - - var result = await sut.GetSchemaOrThrowAsync(requestContext, schemaId.Name); - - Assert.Equal(schema, result); - } - - [Fact] - public async Task Should_throw_404_if_schema_not_found() - { - SetupSchemaNotFound(); - - var ctx = requestContext; - - await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name)); - } - - [Fact] - public async Task Should_throw_404_if_schema_not_found_in_check() - { - SetupSchemaNotFound(); - - var ctx = requestContext; - - await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name)); - } - - [Fact] - public async Task Should_throw_for_single_content_if_no_permission() - { - SetupUser(false, false); - SetupSchemaFound(); - - var ctx = requestContext; - - await Assert.ThrowsAsync(() => sut.FindContentAsync(ctx, schemaId.Name, contentId)); - } - - [Fact] - public async Task Should_throw_404_for_single_content_if_content_not_found() - { - var status = new[] { Status.Published }; - - SetupUser(isFrontend: false); - SetupSchemaFound(); - SetupContent(status, null, includeDraft: false); - - var ctx = requestContext; - - await Assert.ThrowsAsync(async () => await sut.FindContentAsync(ctx, schemaId.Name, contentId)); - } - - [Fact] - public async Task Should_return_single_content_for_frontend_without_transform() - { - var content = CreateContent(contentId); - - SetupUser(isFrontend: true); - SetupSchemaFound(); - SetupSchemaScripting(contentId); - SetupContent(null, content, includeDraft: true); - - var ctx = requestContext; - - var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId); - - Assert.Equal(contentTransformed, result.Data); - Assert.Equal(content.Id, result.Id); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Theory] - [MemberData(nameof(ApiStatusTests))] - public async Task Should_return_single_content_for_api_with_transform(int unpublished, Status[] status) - { - var content = CreateContent(contentId); - - SetupUser(isFrontend: false); - SetupSchemaFound(); - SetupSchemaScripting(contentId); - SetupContent(status, content, unpublished == 1); - - var ctx = requestContext.WithUnpublished(unpublished == 1); - - var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId); - - Assert.Equal(contentTransformed, result.Data); - Assert.Equal(content.Id, result.Id); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_return_versioned_content_from_repository_and_transform() - { - var content = CreateContent(contentId); - - SetupUser(true); - SetupSchemaFound(); - SetupSchemaScripting(contentId); - - A.CallTo(() => contentVersionLoader.GetAsync(contentId, 10)) - .Returns(content); - - var ctx = requestContext; - - var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId, 10); - - Assert.Equal(contentTransformed, result.Data); - Assert.Equal(content.Id, result.Id); - } - - [Fact] - public async Task Should_throw_for_query_if_no_permission() - { - SetupUser(false, false); - SetupSchemaFound(); - - var ctx = requestContext; - - await Assert.ThrowsAsync(() => sut.QueryAsync(ctx, schemaId.Name, Q.Empty)); - } - - [Fact] - public async Task Should_query_contents_by_query_for_frontend_without_transform() - { - const int count = 5, total = 200; - - var content = CreateContent(contentId); - - SetupUser(isFrontend: true); - SetupSchemaFound(); - SetupSchemaScripting(contentId); - SetupContents(null, count, total, content, inDraft: true, includeDraft: true); - - var ctx = requestContext; - - var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty); - - Assert.Equal(contentData, result[0].Data); - Assert.Equal(content.Id, result[0].Id); - - Assert.Equal(total, result.Total); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Theory] - [MemberData(nameof(ApiStatusTests))] - public async Task Should_query_contents_by_query_for_api_and_transform(int unpublished, Status[] status) - { - const int count = 5, total = 200; - - var content = CreateContent(contentId); - - SetupUser(isFrontend: false); - SetupSchemaFound(); - SetupSchemaScripting(contentId); - SetupContents(status, count, total, content, inDraft: false, unpublished == 1); - - var ctx = requestContext.WithUnpublished(unpublished == 1); - - var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty); - - Assert.Equal(contentData, result[0].Data); - Assert.Equal(contentId, result[0].Id); - - Assert.Equal(total, result.Total); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustHaveHappened(count, Times.Exactly); - } - - [Fact] - public async Task Should_query_contents_by_id_for_frontend_and_transform() - { - const int count = 5, total = 200; - - var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); - - SetupUser(isFrontend: true); - SetupSchemaFound(); - SetupSchemaScripting(ids.ToArray()); - SetupContents(null, total, ids, includeDraft: true); - - var ctx = requestContext; - - var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty.WithIds(ids)); - - Assert.Equal(ids, result.Select(x => x.Id).ToList()); - Assert.Equal(total, result.Total); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Theory] - [MemberData(nameof(ApiStatusTests))] - public async Task Should_query_contents_by_id_for_api_and_transform(int unpublished, Status[] status) - { - const int count = 5, total = 200; - - var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); - - SetupUser(isFrontend: false); - SetupSchemaFound(); - SetupSchemaScripting(ids.ToArray()); - SetupContents(status, total, ids, unpublished == 1); - - var ctx = requestContext.WithUnpublished(unpublished == 1); - - var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty.WithIds(ids)); - - Assert.Equal(ids, result.Select(x => x.Id).ToList()); - Assert.Equal(total, result.Total); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustHaveHappened(count, Times.Exactly); - } - - [Fact] - public async Task Should_query_all_contents_by_id_for_frontend_and_transform() - { - const int count = 5; - - var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); - - SetupUser(isFrontend: true); - SetupSchemaFound(); - SetupSchemaScripting(ids.ToArray()); - SetupContents(null, ids, includeDraft: true); - - var ctx = requestContext; - - var result = await sut.QueryAsync(ctx, ids); - - Assert.Equal(ids, result.Select(x => x.Id).ToList()); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Theory] - [MemberData(nameof(ApiStatusTests))] - public async Task Should_query_all_contents_by_id_for_api_and_transform(int unpublished, Status[] status) - { - const int count = 5; - - var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); - - SetupUser(isFrontend: false); - SetupSchemaFound(); - SetupSchemaScripting(ids.ToArray()); - SetupContents(status, ids, unpublished == 1); - - var ctx = requestContext.WithUnpublished(unpublished == 1); - - var result = await sut.QueryAsync(ctx, ids); - - Assert.Equal(ids, result.Select(x => x.Id).ToList()); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustHaveHappened(count, Times.Exactly); - } - - [Fact] - public async Task Should_skip_contents_when_user_has_no_permission() - { - var ids = Enumerable.Range(0, 1).Select(x => Guid.NewGuid()).ToList(); - - SetupUser(isFrontend: false, allowSchema: false); - SetupSchemaFound(); - SetupSchemaScripting(ids.ToArray()); - SetupContents(new Status[0], ids, includeDraft: false); - - var ctx = requestContext; - - var result = await sut.QueryAsync(ctx, ids); - - Assert.Empty(result); - } - - [Fact] - public async Task Should_not_call_repository_if_no_id_defined() - { - var ids = new List(); - - SetupUser(isFrontend: false, allowSchema: false); - SetupSchemaFound(); - - var ctx = requestContext; - - var result = await sut.QueryAsync(ctx, ids); - - Assert.Empty(result); - - A.CallTo(() => contentRepository.QueryAsync(app, A.Ignored, A>.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - private void SetupUser(bool isFrontend, bool allowSchema = true) - { - if (isFrontend) - { - identity.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); - } - - if (allowSchema) - { - identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.ForApp(Permissions.AppContentsRead, app.Name, schema.SchemaDef.Name).Id)); - } - - requestContext.UpdatePermissions(); - } - - private void SetupSchemaScripting(params Guid[] ids) - { - foreach (var id in ids) - { - A.CallTo(() => scriptEngine.Transform(A.That.Matches(x => x.User == user && x.ContentId == id && x.Data == contentData), "")) - .Returns(contentTransformed); - } - } - - private void SetupContents(Status[] status, int count, int total, IContentEntity content, bool inDraft, bool includeDraft) - { - A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.Is(status), inDraft, A.Ignored, includeDraft)) - .Returns(ResultList.Create(total, Enumerable.Repeat(content, count))); - } - - private void SetupContents(Status[] status, int total, List ids, bool includeDraft) - { - A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.Is(status), A>.Ignored, includeDraft)) - .Returns(ResultList.Create(total, ids.Select(CreateContent).Shuffle())); - } - - private void SetupContents(Status[] status, List ids, bool includeDraft) - { - A.CallTo(() => contentRepository.QueryAsync(app, A.That.Is(status), A>.Ignored, includeDraft)) - .Returns(ids.Select(x => (CreateContent(x), schema)).ToList()); - } - - private void SetupContent(Status[] status, IContentEntity content, bool includeDraft) - { - A.CallTo(() => contentRepository.FindContentAsync(app, schema, A.That.Is(status), contentId, includeDraft)) - .Returns(content); - } - - private void SetupSchemaFound() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name)) - .Returns(schema); - - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) - .Returns(schema); - } - - private void SetupSchemaNotFound() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name)) - .Returns((ISchemaEntity)null); - - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) - .Returns((ISchemaEntity)null); - } - - private void SetupEnricher() - { - A.CallTo(() => contentEnricher.EnrichAsync(A>.Ignored, requestContext)) - .ReturnsLazily(x => - { - var input = (IEnumerable)x.Arguments[0]; - - return Task.FromResult>(input.Select(c => SimpleMapper.Map(c, new ContentEntity())).ToList()); - }); - } - - private IContentEntity CreateContent(Guid id) - { - return CreateContent(id, Status.Published); - } - - private IContentEntity CreateContent(Guid id, Status status) - { - var content = new ContentEntity - { - Id = id, - Data = contentData, - DataDraft = contentData, - SchemaId = schemaId, - Status = status - }; - - return content; - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs deleted file mode 100644 index a566fbeec..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using FakeItEasy; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public class FilterTagTransformerTests - { - private readonly ITagService tagService = A.Fake(); - private readonly ISchemaEntity schema; - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - - public FilterTagTransformerTests() - { - var schemaDef = - new Schema("schema") - .AddTags(1, "tags1", Partitioning.Invariant) - .AddTags(2, "tags2", Partitioning.Invariant, new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) - .AddString(3, "string", Partitioning.Invariant); - - schema = Mocks.Schema(appId, schemaId, schemaDef); - } - - [Fact] - public void Should_normalize_tags() - { - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Schemas(schemaId.Id), A>.That.Contains("name1"))) - .Returns(new Dictionary { ["name1"] = "id1" }); - - var source = ClrFilter.Eq("data.tags2.iv", "name1"); - - var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); - - Assert.Equal("data.tags2.iv == 'id1'", result.ToString()); - } - - [Fact] - public void Should_not_fail_when_tags_not_found() - { - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A>.That.Contains("name1"))) - .Returns(new Dictionary()); - - var source = ClrFilter.Eq("data.tags2.iv", "name1"); - - var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); - - Assert.Equal("data.tags2.iv == 'name1'", result.ToString()); - } - - [Fact] - public void Should_not_normalize_other_tags_field() - { - var source = ClrFilter.Eq("data.tags1.iv", "value"); - - var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); - - Assert.Equal("data.tags1.iv == 'value'", result.ToString()); - - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A.Ignored, A>.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public void Should_not_normalize_other_typed_field() - { - var source = ClrFilter.Eq("data.string.iv", "value"); - - var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); - - Assert.Equal("data.string.iv == 'value'", result.ToString()); - - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A.Ignored, A>.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public void Should_not_normalize_non_data_field() - { - var source = ClrFilter.Eq("no.data", "value"); - - var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); - - Assert.Equal("no.data == 'value'", result.ToString()); - - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A.Ignored, A>.Ignored)) - .MustNotHaveHappened(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerGrainTests.cs deleted file mode 100644 index ef46b89e5..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerGrainTests.cs +++ /dev/null @@ -1,263 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - public class TextIndexerGrainTests : IDisposable - { - private readonly Guid schemaId = Guid.NewGuid(); - private readonly List ids1 = new List { Guid.NewGuid() }; - private readonly List ids2 = new List { Guid.NewGuid() }; - private readonly SearchContext context; - private readonly IAssetStore assetStore = new MemoryAssetStore(); - private readonly TextIndexerGrain sut; - - public TextIndexerGrainTests() - { - context = new SearchContext - { - Languages = new HashSet { "de", "en" } - }; - - sut = new TextIndexerGrain(assetStore); - sut.ActivateAsync(schemaId).Wait(); - } - - public void Dispose() - { - sut.OnDeactivateAsync().Wait(); - } - - [Fact] - public async Task Should_throw_exception_for_invalid_query() - { - await Assert.ThrowsAsync(() => sut.SearchAsync("~hello", context)); - } - - [Fact] - public async Task Should_read_index_and_retrieve() - { - await AddInvariantContent("Hello", "World", false); - - await sut.DeactivateAsync(true); - - var other = new TextIndexerGrain(assetStore); - try - { - await other.ActivateAsync(schemaId); - - await TestSearchAsync(ids1, "Hello", grain: other); - await TestSearchAsync(ids2, "World", grain: other); - } - finally - { - await other.OnDeactivateAsync(); - } - } - - [Fact] - public async Task Should_index_invariant_content_and_retrieve() - { - await AddInvariantContent("Hello", "World", false); - - await TestSearchAsync(ids1, "Hello"); - await TestSearchAsync(ids2, "World"); - } - - [Fact] - public async Task Should_index_invariant_content_and_retrieve_with_fuzzy() - { - await AddInvariantContent("Hello", "World", false); - - await TestSearchAsync(ids1, "helo~"); - await TestSearchAsync(ids2, "wold~"); - } - - [Fact] - public async Task Should_update_draft_only() - { - await AddInvariantContent("Hello", "World", false); - await AddInvariantContent("Hallo", "Welt", false); - - await TestSearchAsync(null, "Hello", Scope.Draft); - await TestSearchAsync(null, "Hello", Scope.Published); - - await TestSearchAsync(ids1, "Hallo", Scope.Draft); - await TestSearchAsync(null, "Hallo", Scope.Published); - } - - [Fact] - public async Task Should_also_update_published_after_copy() - { - await AddInvariantContent("Hello", "World", false); - - await CopyAsync(true); - - await AddInvariantContent("Hallo", "Welt", false); - - await TestSearchAsync(null, "Hello", Scope.Draft); - await TestSearchAsync(null, "Hello", Scope.Published); - - await TestSearchAsync(ids1, "Hallo", Scope.Draft); - await TestSearchAsync(ids1, "Hallo", Scope.Published); - } - - [Fact] - public async Task Should_simulate_content_reversion() - { - await AddInvariantContent("Hello", "World", false); - - await CopyAsync(true); - - await AddInvariantContent("Hallo", "Welt", true); - - await TestSearchAsync(null, "Hello", Scope.Draft); - await TestSearchAsync(ids1, "Hello", Scope.Published); - - await TestSearchAsync(ids1, "Hallo", Scope.Draft); - await TestSearchAsync(null, "Hallo", Scope.Published); - - await CopyAsync(false); - - await TestSearchAsync(ids1, "Hello", Scope.Draft); - await TestSearchAsync(ids1, "Hello", Scope.Published); - - await TestSearchAsync(null, "Hallo", Scope.Draft); - await TestSearchAsync(null, "Hallo", Scope.Published); - - await AddInvariantContent("Guten Morgen", "Welt", true); - - await TestSearchAsync(null, "Hello", Scope.Draft); - await TestSearchAsync(ids1, "Hello", Scope.Published); - - await TestSearchAsync(ids1, "Guten Morgen", Scope.Draft); - await TestSearchAsync(null, "Guten Morgen", Scope.Published); - } - - [Fact] - public async Task Should_also_retrieve_published_content_after_copy() - { - await AddInvariantContent("Hello", "World", false); - - await TestSearchAsync(ids1, "Hello", Scope.Draft); - await TestSearchAsync(null, "Hello", Scope.Published); - - await CopyAsync(true); - - await TestSearchAsync(ids1, "Hello", Scope.Draft); - await TestSearchAsync(ids1, "Hello", Scope.Published); - } - - [Fact] - public async Task Should_delete_documents_from_index() - { - await AddInvariantContent("Hello", "World", false); - - await TestSearchAsync(ids1, "Hello"); - await TestSearchAsync(ids2, "World"); - - await DeleteAsync(ids1[0]); - - await TestSearchAsync(null, "Hello"); - await TestSearchAsync(ids2, "World"); - } - - [Fact] - public async Task Should_search_by_field() - { - await AddLocalizedContent(); - - await TestSearchAsync(null, "de:city"); - await TestSearchAsync(null, "en:Stadt"); - } - - [Fact] - public async Task Should_index_localized_content_and_retrieve() - { - await AddLocalizedContent(); - - await TestSearchAsync(ids1, "Stadt"); - await TestSearchAsync(ids1, "and"); - await TestSearchAsync(ids2, "und"); - - await TestSearchAsync(ids2, "City"); - await TestSearchAsync(ids2, "und"); - await TestSearchAsync(ids1, "and"); - } - - private async Task AddLocalizedContent() - { - var germanData = - new NamedContentData() - .AddField("localized", - new ContentFieldData() - .AddValue("de", "Stadt und Umgebung and whatever")); - - var englishData = - new NamedContentData() - .AddField("localized", - new ContentFieldData() - .AddValue("en", "City and Surroundings und sonstiges")); - - await sut.IndexAsync(new Update { Id = ids1[0], Data = germanData, OnlyDraft = true }); - await sut.IndexAsync(new Update { Id = ids2[0], Data = englishData, OnlyDraft = true }); - } - - private async Task AddInvariantContent(string text1, string text2, bool onlyDraft = false) - { - var data1 = - new NamedContentData() - .AddField("test", - new ContentFieldData() - .AddValue("iv", text1)); - - var data2 = - new NamedContentData() - .AddField("test", - new ContentFieldData() - .AddValue("iv", text2)); - - await sut.IndexAsync(new Update { Id = ids1[0], Data = data1, OnlyDraft = onlyDraft }); - await sut.IndexAsync(new Update { Id = ids2[0], Data = data2, OnlyDraft = onlyDraft }); - } - - private async Task DeleteAsync(Guid id) - { - await sut.DeleteAsync(id); - } - - private async Task CopyAsync(bool fromDraft) - { - await sut.CopyAsync(ids1[0], fromDraft); - await sut.CopyAsync(ids2[0], fromDraft); - } - - private async Task TestSearchAsync(List expected, string text, Scope target = Scope.Draft, TextIndexerGrain grain = null) - { - context.Scope = target; - - var result = await (grain ?? sut).SearchAsync(text, context); - - if (expected != null) - { - Assert.Equal(expected, result); - } - else - { - Assert.Empty(result); - } - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs deleted file mode 100644 index e0bf381d2..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs +++ /dev/null @@ -1,191 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using NodaTime; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Shared.Users; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.History.Notifications -{ - public class NotificationEmailEventConsumerTests - { - private readonly INotificationEmailSender emailSender = A.Fake(); - private readonly IUserResolver userResolver = A.Fake(); - private readonly IUser assigner = A.Fake(); - private readonly IUser assignee = A.Fake(); - private readonly ISemanticLog log = A.Fake(); - private readonly string assignerId = Guid.NewGuid().ToString(); - private readonly string assigneeId = Guid.NewGuid().ToString(); - private readonly string appName = "my-app"; - private readonly NotificationEmailEventConsumer sut; - - public NotificationEmailEventConsumerTests() - { - A.CallTo(() => emailSender.IsActive) - .Returns(true); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId)) - .Returns(assigner); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId)) - .Returns(assignee); - - sut = new NotificationEmailEventConsumer(emailSender, userResolver, log); - } - - [Fact] - public async Task Should_not_send_email_if_contributors_assigned_by_clients() - { - var @event = CreateEvent(RefTokenType.Client, true); - - await sut.On(@event); - - MustNotResolveUser(); - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_for_initial_owner() - { - var @event = CreateEvent(RefTokenType.Subject, false, streamNumber: 1); - - await sut.On(@event); - - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_for_old_events() - { - var @event = CreateEvent(RefTokenType.Subject, true, instant: SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(50))); - - await sut.On(@event); - - MustNotResolveUser(); - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_for_old_contributor() - { - var @event = CreateEvent(RefTokenType.Subject, true, isNewContributor: false); - - await sut.On(@event); - - MustNotResolveUser(); - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_if_sender_not_active() - { - var @event = CreateEvent(RefTokenType.Subject, true); - - A.CallTo(() => emailSender.IsActive) - .Returns(false); - - await sut.On(@event); - - MustNotResolveUser(); - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_if_assigner_not_found() - { - var @event = CreateEvent(RefTokenType.Subject, true); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId)) - .Returns(Task.FromResult(null)); - - await sut.On(@event); - - MustNotSendEmail(); - MustLogWarning(); - } - - [Fact] - public async Task Should_not_send_email_if_assignee_not_found() - { - var @event = CreateEvent(RefTokenType.Subject, true); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId)) - .Returns(Task.FromResult(null)); - - await sut.On(@event); - - MustNotSendEmail(); - MustLogWarning(); - } - - [Fact] - public async Task Should_send_email_for_new_user() - { - var @event = CreateEvent(RefTokenType.Subject, true); - - await sut.On(@event); - - A.CallTo(() => emailSender.SendContributorEmailAsync(assigner, assignee, appName, true)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_send_email_for_existing_user() - { - var @event = CreateEvent(RefTokenType.Subject, false); - - await sut.On(@event); - - A.CallTo(() => emailSender.SendContributorEmailAsync(assigner, assignee, appName, false)) - .MustHaveHappened(); - } - - private void MustLogWarning() - { - A.CallTo(() => log.Log(SemanticLogLevel.Warning, A.Ignored, A>.Ignored)) - .MustHaveHappened(); - } - - private void MustNotResolveUser() - { - A.CallTo(() => userResolver.FindByIdOrEmailAsync(A.Ignored)) - .MustNotHaveHappened(); - } - - private void MustNotSendEmail() - { - A.CallTo(() => emailSender.SendContributorEmailAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - private Envelope CreateEvent(string assignerType, bool isNewUser, bool isNewContributor = true, Instant? instant = null, int streamNumber = 2) - { - var @event = new AppContributorAssigned - { - Actor = new RefToken(assignerType, assignerId), - AppId = NamedId.Of(Guid.NewGuid(), appName), - ContributorId = assigneeId, - IsCreated = isNewUser, - IsAdded = isNewContributor - }; - - var envelope = Envelope.Create(@event); - - envelope.SetTimestamp(instant ?? SystemClock.Instance.GetCurrentInstant()); - envelope.SetEventStreamNumber(streamNumber); - - return envelope; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs deleted file mode 100644 index 263b27a7a..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs +++ /dev/null @@ -1,188 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; -using Squidex.Infrastructure.Validation; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Entities.Rules.Guards -{ - public class GuardRuleTests - { - private readonly Uri validUrl = new Uri("https://squidex.io"); - private readonly Rule rule_0 = new Rule(new ContentChangedTriggerV2(), new TestAction()).Rename("MyName"); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly IAppProvider appProvider = A.Fake(); - - public sealed class TestAction : RuleAction - { - public Uri Url { get; set; } - } - - public GuardRuleTests() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) - .Returns(Mocks.Schema(appId, schemaId)); - } - - [Fact] - public async Task CanCreate_should_throw_exception_if_trigger_null() - { - var command = CreateCommand(new CreateRule - { - Trigger = null, - Action = new TestAction - { - Url = validUrl - } - }); - - await ValidationAssert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider), - new ValidationError("Trigger is required.", "Trigger")); - } - - [Fact] - public async Task CanCreate_should_throw_exception_if_action_null() - { - var command = CreateCommand(new CreateRule - { - Trigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Empty() - }, - Action = null - }); - - await ValidationAssert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider), - new ValidationError("Action is required.", "Action")); - } - - [Fact] - public async Task CanCreate_should_not_throw_exception_if_trigger_and_action_valid() - { - var command = CreateCommand(new CreateRule - { - Trigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Empty() - }, - Action = new TestAction - { - Url = validUrl - } - }); - - await GuardRule.CanCreate(command, appProvider); - } - - [Fact] - public async Task CanUpdate_should_throw_exception_if_action_and_trigger_are_null() - { - var command = new UpdateRule(); - - await ValidationAssert.ThrowsAsync(() => GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0), - new ValidationError("Either trigger, action or name is required.", "Trigger", "Action")); - } - - [Fact] - public async Task CanUpdate_should_throw_exception_if_rule_has_already_this_name() - { - var command = new UpdateRule - { - Name = "MyName" - }; - - await ValidationAssert.ThrowsAsync(() => GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0), - new ValidationError("Rule has already this name.", "Name")); - } - - [Fact] - public async Task CanUpdate_should_not_throw_exception_if_trigger_action__and_name_are_valid() - { - var command = new UpdateRule - { - Trigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Empty() - }, - Action = new TestAction - { - Url = validUrl - }, - Name = "NewName" - }; - - await GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0); - } - - [Fact] - public void CanEnable_should_throw_exception_if_rule_enabled() - { - var command = new EnableRule(); - - var rule_1 = rule_0.Enable(); - - Assert.Throws(() => GuardRule.CanEnable(command, rule_1)); - } - - [Fact] - public void CanEnable_should_not_throw_exception_if_rule_disabled() - { - var command = new EnableRule(); - - var rule_1 = rule_0.Disable(); - - GuardRule.CanEnable(command, rule_1); - } - - [Fact] - public void CanDisable_should_throw_exception_if_rule_disabled() - { - var command = new DisableRule(); - - var rule_1 = rule_0.Disable(); - - Assert.Throws(() => GuardRule.CanDisable(command, rule_1)); - } - - [Fact] - public void CanDisable_should_not_throw_exception_if_rule_enabled() - { - var command = new DisableRule(); - - var rule_1 = rule_0.Enable(); - - GuardRule.CanDisable(command, rule_1); - } - - [Fact] - public void CanDelete_should_not_throw_exception() - { - var command = new DeleteRule(); - - GuardRule.CanDelete(command); - } - - private CreateRule CreateCommand(CreateRule command) - { - command.AppId = appId; - - return command; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs deleted file mode 100644 index e871bf736..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers -{ - public class ContentChangedTriggerTests - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - - [Fact] - public async Task Should_add_error_if_schema_id_is_not_defined() - { - var trigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2()) - }; - - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - - errors.Should().BeEquivalentTo( - new List - { - new ValidationError("Schema id is required.", "Schemas") - }); - - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_add_error_if_schemas_ids_are_not_valid() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) - .Returns(Task.FromResult(null)); - - var trigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId.Id }) - }; - - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - - errors.Should().BeEquivalentTo( - new List - { - new ValidationError($"Schema {schemaId.Id} does not exist.", "Schemas") - }); - } - - [Fact] - public async Task Should_not_add_error_if_schemas_is_null() - { - var trigger = new ContentChangedTriggerV2(); - - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_schemas_is_empty() - { - var trigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Empty() - }; - - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_schemas_ids_are_valid() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) - .Returns(Mocks.Schema(appId, schemaId)); - - var trigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId.Id }) - }; - - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - - Assert.Empty(errors); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs deleted file mode 100644 index 569ca61f7..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Infrastructure.EventSourcing; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public class ManualTriggerHandlerTests - { - private readonly IRuleTriggerHandler sut = new ManualTriggerHandler(); - - [Fact] - public async Task Should_create_event_with_name() - { - var envelope = Envelope.Create(new RuleManuallyTriggered()); - - var result = await sut.CreateEnrichedEventAsync(envelope); - - Assert.Equal("Manual", result.Name); - } - - [Fact] - public void Should_always_trigger() - { - Assert.True(sut.Trigger(new EnrichedManualEvent(), new ManualTrigger())); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs deleted file mode 100644 index b9455635c..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public class RuleEnqueuerTests - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - private readonly IRuleEventRepository ruleEventRepository = A.Fake(); - private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly RuleService ruleService = A.Fake(); - private readonly RuleEnqueuer sut; - - public sealed class TestAction : RuleAction - { - public Uri Url { get; set; } - } - - public RuleEnqueuerTests() - { - sut = new RuleEnqueuer( - appProvider, - cache, - ruleEventRepository, - ruleService); - } - - [Fact] - public void Should_return_contents_filter_for_events_filter() - { - Assert.Equal(".*", sut.EventsFilter); - } - - [Fact] - public void Should_return_type_name_for_name() - { - Assert.Equal(typeof(RuleEnqueuer).Name, sut.Name); - } - - [Fact] - public async Task Should_do_nothing_on_clear() - { - await sut.ClearAsync(); - } - - [Fact] - public async Task Should_update_repository_when_enqueing() - { - var @event = Envelope.Create(new ContentCreated { AppId = appId }); - - var rule = CreateRule(); - - var job = new RuleJob { Created = now }; - - A.CallTo(() => ruleService.CreateJobAsync(rule.RuleDef, rule.Id, @event)) - .Returns(job); - - await sut.Enqueue(rule.RuleDef, rule.Id, @event); - - A.CallTo(() => ruleEventRepository.EnqueueAsync(job, now)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_update_repositories_with_jobs_from_service() - { - var @event = Envelope.Create(new ContentCreated { AppId = appId }); - - var rule1 = CreateRule(); - var rule2 = CreateRule(); - - var job1 = new RuleJob { Created = now }; - - A.CallTo(() => appProvider.GetRulesAsync(appId.Id)) - .Returns(new List { rule1, rule2 }); - - A.CallTo(() => ruleService.CreateJobAsync(rule1.RuleDef, rule1.Id, @event)) - .Returns(job1); - - A.CallTo(() => ruleService.CreateJobAsync(rule2.RuleDef, rule2.Id, @event)) - .Returns(Task.FromResult(null)); - - await sut.On(@event); - - A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, now)) - .MustHaveHappened(); - } - - private static RuleEntity CreateRule() - { - var rule = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") }); - - return new RuleEntity { RuleDef = rule, Id = Guid.NewGuid() }; - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs deleted file mode 100644 index b8ca9421b..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure.EventSourcing; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking -{ - public class UsageTriggerHandlerTests - { - private readonly Guid ruleId = Guid.NewGuid(); - private readonly IRuleTriggerHandler sut = new UsageTriggerHandler(); - - [Fact] - public void Should_not_trigger_precheck_when_event_type_not_correct() - { - var result = sut.Trigger(new ContentCreated(), new UsageTrigger(), ruleId); - - Assert.False(result); - } - - [Fact] - public void Should_not_trigger_precheck_when_rule_id_not_matchs() - { - var result = sut.Trigger(new AppUsageExceeded { RuleId = Guid.NewGuid() }, new UsageTrigger(), ruleId); - - Assert.True(result); - } - - [Fact] - public void Should_trigger_precheck_when_event_type_correct_and_rule_id_matchs() - { - var result = sut.Trigger(new AppUsageExceeded { RuleId = ruleId }, new UsageTrigger(), ruleId); - - Assert.True(result); - } - - [Fact] - public void Should_not_trigger_check_when_event_type_not_correct() - { - var result = sut.Trigger(new EnrichedContentEvent(), new UsageTrigger()); - - Assert.False(result); - } - - [Fact] - public async Task Should_create_enriched_event() - { - var @event = new AppUsageExceeded { CallsCurrent = 80, CallsLimit = 120 }; - - var result = (EnrichedUsageExceededEvent)await sut.CreateEnrichedEventAsync(Envelope.Create(@event)); - - Assert.Equal(@event.CallsCurrent, result.CallsCurrent); - Assert.Equal(@event.CallsLimit, result.CallsLimit); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs deleted file mode 100644 index 5320d862d..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs +++ /dev/null @@ -1,379 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore -#pragma warning disable SA1401 // Fields must be private - -namespace Squidex.Domain.Apps.Entities.Schemas.Guards -{ - public class GuardSchemaFieldTests - { - private readonly Schema schema_0; - private readonly StringFieldProperties validProperties = new StringFieldProperties(); - private readonly StringFieldProperties invalidProperties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }; - - public GuardSchemaFieldTests() - { - schema_0 = - new Schema("my-schema") - .AddString(1, "field1", Partitioning.Invariant) - .AddString(2, "field2", Partitioning.Invariant) - .AddArray(3, "field3", Partitioning.Invariant, f => f - .AddNumber(301, "field301")) - .AddUI(4, "field4", Partitioning.Invariant); - } - - private static Action A(Action method) where T : FieldCommand - { - return method; - } - - private static Func S(Func method) - { - return method; - } - - public static IEnumerable FieldCommandData = new[] - { - new object[] { A(GuardSchemaField.CanEnable) }, - new object[] { A(GuardSchemaField.CanDelete) }, - new object[] { A(GuardSchemaField.CanDisable) }, - new object[] { A(GuardSchemaField.CanHide) }, - new object[] { A(GuardSchemaField.CanLock) }, - new object[] { A(GuardSchemaField.CanShow) }, - new object[] { A(GuardSchemaField.CanUpdate) } - }; - - public static IEnumerable InvalidStates = new[] - { - new object[] { A(GuardSchemaField.CanDisable), S(s => s.DisableField(1)) }, - new object[] { A(GuardSchemaField.CanEnable), S(s => s) }, - new object[] { A(GuardSchemaField.CanHide), S(s => s.HideField(1)) }, - new object[] { A(GuardSchemaField.CanShow), S(s => s.LockField(1)) }, - new object[] { A(GuardSchemaField.CanLock), S(s => s.LockField(1)) } - }; - - public static IEnumerable InvalidNestedStates = new[] - { - new object[] { A(GuardSchemaField.CanDisable), S(s => s.DisableField(301, 3)) }, - new object[] { A(GuardSchemaField.CanEnable), S(s => s) }, - new object[] { A(GuardSchemaField.CanHide), S(s => s.HideField(301, 3)) }, - new object[] { A(GuardSchemaField.CanShow), S(s => s) }, - new object[] { A(GuardSchemaField.CanLock), S(s => s.LockField(301, 3)) } - }; - - public static IEnumerable ValidStates = new[] - { - new object[] { A(GuardSchemaField.CanDisable), S(s => s) }, - new object[] { A(GuardSchemaField.CanEnable), S(s => s.DisableField(1)) }, - new object[] { A(GuardSchemaField.CanHide), S(s => s) }, - new object[] { A(GuardSchemaField.CanShow), S(s => s.HideField(1)) } - }; - - public static IEnumerable ValidNestedStates = new[] - { - new object[] { A(GuardSchemaField.CanEnable), S(s => s.DisableField(301, 3)) }, - new object[] { A(GuardSchemaField.CanDisable), S(s => s) }, - new object[] { A(GuardSchemaField.CanHide), S(s => s) }, - new object[] { A(GuardSchemaField.CanShow), S(s => s.HideField(301, 3)) } - }; - - [Theory] - [MemberData(nameof(FieldCommandData))] - public void Commands_should_throw_exception_if_field_not_found(Action action) where T : FieldCommand, new() - { - var command = new T { FieldId = 5 }; - - Assert.Throws(() => action(schema_0, command)); - } - - [Theory] - [MemberData(nameof(FieldCommandData))] - public void Commands_should_throw_exception_if_parent_field_not_found(Action action) where T : FieldCommand, new() - { - var command = new T { ParentFieldId = 4, FieldId = 401 }; - - Assert.Throws(() => action(schema_0, command)); - } - - [Theory] - [MemberData(nameof(FieldCommandData))] - public void Commands_should_throw_exception_if_child_field_not_found(Action action) where T : FieldCommand, new() - { - var command = new T { ParentFieldId = 3, FieldId = 302 }; - - Assert.Throws(() => action(schema_0, command)); - } - - [Theory] - [MemberData(nameof(InvalidStates))] - public void Commands_should_throw_exception_if_state_not_valid(Action action, Func updater) where T : FieldCommand, new() - { - var command = new T { FieldId = 1 }; - - Assert.Throws(() => action(updater(schema_0), command)); - } - - [Theory] - [MemberData(nameof(InvalidNestedStates))] - public void Commands_should_throw_exception_if_nested_state_not_valid(Action action, Func updater) where T : FieldCommand, new() - { - var command = new T { ParentFieldId = 3, FieldId = 301 }; - - Assert.Throws(() => action(updater(schema_0), command)); - } - - [Theory] - [MemberData(nameof(ValidStates))] - public void Commands_should_not_throw_exception_if_state_valid(Action action, Func updater) where T : FieldCommand, new() - { - var command = new T { FieldId = 1 }; - - action(updater(schema_0), command); - } - - [Theory] - [MemberData(nameof(ValidNestedStates))] - public void Commands_should_not_throw_exception_if_nested_state_valid(Action action, Func updater) where T : FieldCommand, new() - { - var command = new T { ParentFieldId = 3, FieldId = 301 }; - - action(updater(schema_0), command); - } - - [Fact] - public void CanDelete_should_throw_exception_if_locked() - { - var command = new DeleteField { FieldId = 1 }; - - var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - - Assert.Throws(() => GuardSchemaField.CanDelete(schema_1, command)); - } - - [Fact] - public void CanDisable_should_throw_exception_if_already_disabled() - { - var command = new DisableField { FieldId = 1 }; - - var schema_1 = schema_0.UpdateField(1, f => f.Disable()); - - Assert.Throws(() => GuardSchemaField.CanDisable(schema_1, command)); - } - - [Fact] - public void CanDisable_should_throw_exception_if_ui_field() - { - var command = new DisableField { FieldId = 4 }; - - Assert.Throws(() => GuardSchemaField.CanDisable(schema_0, command)); - } - - [Fact] - public void CanEnable_should_throw_exception_if_already_enabled() - { - var command = new EnableField { FieldId = 1 }; - - Assert.Throws(() => GuardSchemaField.CanEnable(schema_0, command)); - } - - [Fact] - public void CanHide_should_throw_exception_if_locked() - { - var command = new HideField { FieldId = 1 }; - - var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - - Assert.Throws(() => GuardSchemaField.CanHide(schema_1, command)); - } - - [Fact] - public void CanHide_should_throw_exception_if_already_hidden() - { - var command = new HideField { FieldId = 1 }; - - var schema_1 = schema_0.UpdateField(1, f => f.Hide()); - - Assert.Throws(() => GuardSchemaField.CanHide(schema_1, command)); - } - - [Fact] - public void CanHide_should_throw_exception_if_ui_field() - { - var command = new HideField { FieldId = 4 }; - - Assert.Throws(() => GuardSchemaField.CanHide(schema_0, command)); - } - - [Fact] - public void CanShow_should_throw_exception_if_already_visible() - { - var command = new ShowField { FieldId = 4 }; - - Assert.Throws(() => GuardSchemaField.CanShow(schema_0, command)); - } - - [Fact] - public void CanDelete_should_not_throw_exception_if_not_locked() - { - var command = new DeleteField { FieldId = 1 }; - - GuardSchemaField.CanDelete(schema_0, command); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_locked() - { - var command = new UpdateField { FieldId = 1, Properties = validProperties }; - - var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - - Assert.Throws(() => GuardSchemaField.CanUpdate(schema_1, command)); - } - - [Fact] - public void CanUpdate_should_not_throw_exception_if_not_locked() - { - var command = new UpdateField { FieldId = 1, Properties = validProperties }; - - GuardSchemaField.CanUpdate(schema_0, command); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_marking_a_ui_field_as_list_field() - { - var command = new UpdateField { FieldId = 4, Properties = new UIFieldProperties { IsListField = true } }; - - ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), - new ValidationError("UI field cannot be a list field.", "Properties.IsListField")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_marking_a_ui_field_as_reference_field() - { - var command = new UpdateField { FieldId = 4, Properties = new UIFieldProperties { IsReferenceField = true } }; - - ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), - new ValidationError("UI field cannot be a reference field.", "Properties.IsReferenceField")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_properties_null() - { - var command = new UpdateField { FieldId = 2, Properties = null }; - - ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), - new ValidationError("Properties is required.", "Properties")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_properties_not_valid() - { - var command = new UpdateField { FieldId = 2, Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } }; - - ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), - new ValidationError("Max length must be greater or equal to min length.", "Properties.MinLength", "Properties.MaxLength")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_field_already_exists() - { - var command = new AddField { Name = "field1", Properties = validProperties }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), - new ValidationError("A field with the same name already exists.")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_nested_field_already_exists() - { - var command = new AddField { Name = "field301", Properties = validProperties, ParentFieldId = 3 }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), - new ValidationError("A field with the same name already exists.")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_name_not_valid() - { - var command = new AddField { Name = "INVALID_NAME", Properties = validProperties }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), - new ValidationError("Name must be a valid javascript property name.", "Name")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_properties_not_valid() - { - var command = new AddField { Name = "field5", Properties = invalidProperties }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), - new ValidationError("Max length must be greater or equal to min length.", "Properties.MinLength", "Properties.MaxLength")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_properties_null() - { - var command = new AddField { Name = "field5", Properties = null }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), - new ValidationError("Properties is required.", "Properties")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_partitioning_not_valid() - { - var command = new AddField { Name = "field5", Partitioning = "INVALID_PARTITIONING", Properties = validProperties }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), - new ValidationError("Partitioning is not a valid value.", "Partitioning")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_creating_a_ui_field_as_list_field() - { - var command = new AddField { Name = "field5", Properties = new UIFieldProperties { IsListField = true } }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), - new ValidationError("UI field cannot be a list field.", "Properties.IsListField")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_parent_not_exists() - { - var command = new AddField { Name = "field302", Properties = validProperties, ParentFieldId = 99 }; - - Assert.Throws(() => GuardSchemaField.CanAdd(schema_0, command)); - } - - [Fact] - public void CanAdd_should_not_throw_exception_if_field_not_exists() - { - var command = new AddField { Name = "field5", Properties = validProperties }; - - GuardSchemaField.CanAdd(schema_0, command); - } - - [Fact] - public void CanAdd_should_not_throw_exception_if_field_exists_in_root() - { - var command = new AddField { Name = "field1", Properties = validProperties, ParentFieldId = 3 }; - - GuardSchemaField.CanAdd(schema_0, command); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs deleted file mode 100644 index 557737a10..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs +++ /dev/null @@ -1,530 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Entities.Schemas.Guards -{ - public class GuardSchemaTests - { - private readonly Schema schema_0; - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - - public GuardSchemaTests() - { - schema_0 = - new Schema("my-schema") - .AddString(1, "field1", Partitioning.Invariant) - .AddString(2, "field2", Partitioning.Invariant); - } - - [Fact] - public void CanCreate_should_throw_exception_if_name_not_valid() - { - var command = new CreateSchema { AppId = appId, Name = "INVALID NAME" }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Name is not a valid slug.", "Name")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_field_name_invalid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "invalid name", - Properties = new StringFieldProperties(), - Partitioning = Partitioning.Invariant.Key - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Field name must be a valid javascript property name.", - "Fields[1].Name")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_field_properties_null() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "field1", - Properties = null, - Partitioning = Partitioning.Invariant.Key - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Field properties is required.", - "Fields[1].Properties")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_field_properties_not_valid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }, - Partitioning = Partitioning.Invariant.Key - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Max length must be greater or equal to min length.", - "Fields[1].Properties.MinLength", - "Fields[1].Properties.MaxLength")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_field_partitioning_not_valid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties(), - Partitioning = "INVALID" - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Partitioning is not a valid value.", - "Fields[1].Partitioning")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_fields_contains_duplicate_name() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties(), - Partitioning = Partitioning.Invariant.Key - }, - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties(), - Partitioning = Partitioning.Invariant.Key - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Fields cannot have duplicate names.", - "Fields")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_name_invalid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new List - { - new UpsertSchemaNestedField - { - Name = "invalid name", - Properties = new StringFieldProperties() - } - } - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Field name must be a valid javascript property name.", - "Fields[1].Nested[1].Name")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_properties_null() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new List - { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = null - } - } - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Field properties is required.", - "Fields[1].Nested[1].Properties")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_is_array() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new List - { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = new ArrayFieldProperties() - } - } - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Nested field cannot be array fields.", - "Fields[1].Nested[1].Properties")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_properties_not_valid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new List - { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } - } - } - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Max length must be greater or equal to min length.", - "Fields[1].Nested[1].Properties.MinLength", - "Fields[1].Nested[1].Properties.MaxLength")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_have_duplicate_names() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new List - { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = new StringFieldProperties() - }, - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = new StringFieldProperties() - } - } - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Fields cannot have duplicate names.", - "Fields[1].Nested")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_ui_field_is_invalid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "field1", - Properties = new UIFieldProperties - { - IsListField = true, - IsReferenceField = true - }, - IsHidden = true, - IsDisabled = true, - Partitioning = Partitioning.Invariant.Key - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("UI field cannot be a list field.", - "Fields[1].Properties.IsListField"), - new ValidationError("UI field cannot be a reference field.", - "Fields[1].Properties.IsReferenceField"), - new ValidationError("UI field cannot be hidden.", - "Fields[1].IsHidden"), - new ValidationError("UI field cannot be disabled.", - "Fields[1].IsDisabled")); - } - - [Fact] - public void CanCreate_should_not_throw_exception_if_command_is_valid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties - { - IsListField = true - }, - IsHidden = true, - IsDisabled = true, - Partitioning = Partitioning.Invariant.Key - }, - new UpsertSchemaField - { - Name = "field2", - Properties = ValidProperties(), - Partitioning = Partitioning.Invariant.Key - }, - new UpsertSchemaField - { - Name = "field3", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new List - { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = ValidProperties() - }, - new UpsertSchemaNestedField - { - Name = "nested2", - Properties = ValidProperties() - } - } - } - }, - Name = "new-schema" - }; - - GuardSchema.CanCreate(command); - } - - [Fact] - public void CanPublish_should_throw_exception_if_already_published() - { - var command = new PublishSchema(); - - var schema_1 = schema_0.Publish(); - - Assert.Throws(() => GuardSchema.CanPublish(schema_1, command)); - } - - [Fact] - public void CanPublish_should_not_throw_exception_if_not_published() - { - var command = new PublishSchema(); - - GuardSchema.CanPublish(schema_0, command); - } - - [Fact] - public void CanUnpublish_should_throw_exception_if_already_unpublished() - { - var command = new UnpublishSchema(); - - Assert.Throws(() => GuardSchema.CanUnpublish(schema_0, command)); - } - - [Fact] - public void CanUnpublish_should_not_throw_exception_if_already_published() - { - var command = new UnpublishSchema(); - - var schema_1 = schema_0.Publish(); - - GuardSchema.CanUnpublish(schema_1, command); - } - - [Fact] - public void CanReorder_should_throw_exception_if_field_ids_contains_invalid_id() - { - var command = new ReorderFields { FieldIds = new List { 1, 3 } }; - - ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), - new ValidationError("Field ids do not cover all fields.", "FieldIds")); - } - - [Fact] - public void CanReorder_should_throw_exception_if_field_ids_do_not_covers_all_fields() - { - var command = new ReorderFields { FieldIds = new List { 1 } }; - - ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), - new ValidationError("Field ids do not cover all fields.", "FieldIds")); - } - - [Fact] - public void CanReorder_should_throw_exception_if_field_ids_null() - { - var command = new ReorderFields { FieldIds = null }; - - ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), - new ValidationError("Field ids is required.", "FieldIds")); - } - - [Fact] - public void CanReorder_should_throw_exception_if_parent_field_not_found() - { - var command = new ReorderFields { FieldIds = new List { 1, 2 }, ParentFieldId = 99 }; - - Assert.Throws(() => GuardSchema.CanReorder(schema_0, command)); - } - - [Fact] - public void CanReorder_should_not_throw_exception_if_field_ids_are_valid() - { - var command = new ReorderFields { FieldIds = new List { 1, 2 } }; - - GuardSchema.CanReorder(schema_0, command); - } - - [Fact] - public void CanConfigurePreviewUrls_should_throw_exception_if_preview_urls_null() - { - var command = new ConfigurePreviewUrls { PreviewUrls = null }; - - ValidationAssert.Throws(() => GuardSchema.CanConfigurePreviewUrls(command), - new ValidationError("Preview Urls is required.", "PreviewUrls")); - } - - [Fact] - public void CanConfigurePreviewUrls_should_not_throw_exception_if_valid() - { - var command = new ConfigurePreviewUrls { PreviewUrls = new Dictionary() }; - - GuardSchema.CanConfigurePreviewUrls(command); - } - - [Fact] - public void CanChangeCategory_should_not_throw_exception() - { - var command = new ChangeCategory(); - - GuardSchema.CanChangeCategory(schema_0, command); - } - - [Fact] - public void CanDelete_should_not_throw_exception() - { - var command = new DeleteSchema(); - - GuardSchema.CanDelete(schema_0, command); - } - - private static StringFieldProperties ValidProperties() - { - return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs deleted file mode 100644 index 7f73232cf..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs +++ /dev/null @@ -1,248 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes -{ - public class SchemasIndexTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly ISchemasByAppIndexGrain index = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly SchemasIndex sut; - - public SchemasIndexTests() - { - A.CallTo(() => grainFactory.GetGrain(appId.Id, null)) - .Returns(index); - - sut = new SchemasIndex(grainFactory); - } - - [Fact] - public async Task Should_resolve_schema_by_id() - { - var schema = SetupSchema(0, false); - - var actual = await sut.GetSchemaAsync(appId.Id, schema.Id); - - Assert.Same(actual, schema); - } - - [Fact] - public async Task Should_resolve_schema_by_name() - { - var schema = SetupSchema(0, false); - - A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) - .Returns(schema.Id); - - var actual = await sut.GetSchemaByNameAsync(appId.Id, schema.SchemaDef.Name); - - Assert.Same(actual, schema); - } - - [Fact] - public async Task Should_resolve_schemas_by_id() - { - var schema = SetupSchema(0, false); - - A.CallTo(() => index.GetIdsAsync()) - .Returns(new List { schema.Id }); - - var actual = await sut.GetSchemasAsync(appId.Id); - - Assert.Same(actual[0], schema); - } - - [Fact] - public async Task Should_return_empty_schema_if_schema_not_created() - { - var schema = SetupSchema(-1, false); - - A.CallTo(() => index.GetIdsAsync()) - .Returns(new List { schema.Id }); - - var actual = await sut.GetSchemasAsync(appId.Id); - - Assert.Empty(actual); - } - - [Fact] - public async Task Should_return_empty_schema_if_schema_deleted() - { - var schema = SetupSchema(0, true); - - A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) - .Returns(schema.Id); - - var actual = await sut.GetSchemasAsync(appId.Id); - - Assert.Empty(actual); - } - - [Fact] - public async Task Should_also_return_schema_if_deleted_allowed() - { - var schema = SetupSchema(-1, true); - - A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) - .Returns(schema.Id); - - var actual = await sut.GetSchemasAsync(appId.Id, true); - - Assert.Empty(actual); - } - - [Fact] - public async Task Should_add_schema_to_index_on_create() - { - var token = RandomHash.Simple(); - - A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) - .Returns(token); - - var context = - new CommandContext(Create(schemaId.Name), commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.AddAsync(token)) - .MustHaveHappened(); - - A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_clear_reservation_when_schema_creation_failed() - { - var token = RandomHash.Simple(); - - A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) - .Returns(token); - - var context = - new CommandContext(Create(schemaId.Name), commandBus); - - await sut.HandleAsync(context); - - A.CallTo(() => index.AddAsync(token)) - .MustNotHaveHappened(); - - A.CallTo(() => index.RemoveReservationAsync(token)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_add_to_index_on_create_if_name_taken() - { - A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) - .Returns(Task.FromResult(null)); - - var context = - new CommandContext(Create(schemaId.Name), commandBus) - .Complete(); - - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - - A.CallTo(() => index.AddAsync(A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_add_to_index_on_create_if_name_invalid() - { - var context = - new CommandContext(Create("INVALID"), commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.ReserveAsync(schemaId.Id, A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_remove_schema_from_index_on_delete() - { - var schema = SetupSchema(0, false); - - var context = - new CommandContext(new DeleteSchema { SchemaId = schema.Id }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.RemoveAsync(schema.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_call_when_rebuilding() - { - var schemas = new Dictionary(); - - await sut.RebuildAsync(appId.Id, schemas); - - A.CallTo(() => index.RebuildAsync(schemas)) - .MustHaveHappened(); - } - - private CreateSchema Create(string name) - { - return new CreateSchema { SchemaId = schemaId.Id, Name = name, AppId = appId }; - } - - private ISchemaEntity SetupSchema(long version, bool deleted) - { - var schemaEntity = A.Fake(); - - A.CallTo(() => schemaEntity.SchemaDef) - .Returns(new Schema(schemaId.Name)); - A.CallTo(() => schemaEntity.Id) - .Returns(schemaId.Id); - A.CallTo(() => schemaEntity.AppId) - .Returns(appId); - A.CallTo(() => schemaEntity.Version) - .Returns(version); - A.CallTo(() => schemaEntity.IsDeleted) - .Returns(deleted); - - var schemaGrain = A.Fake(); - - A.CallTo(() => schemaGrain.GetStateAsync()) - .Returns(J.Of(schemaEntity)); - - A.CallTo(() => grainFactory.GetGrain(schemaId.Id, null)) - .Returns(schemaGrain); - - return schemaEntity; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs deleted file mode 100644 index 2501192a3..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs +++ /dev/null @@ -1,146 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure.EventSourcing; -using Xunit; - -#pragma warning disable SA1401 // Fields must be private - -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public class SchemaChangedTriggerHandlerTests - { - private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly IRuleTriggerHandler sut; - - public SchemaChangedTriggerHandlerTests() - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) - .Returns(true); - - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) - .Returns(false); - - sut = new SchemaChangedTriggerHandler(scriptEngine); - } - - public static IEnumerable TestEvents = new[] - { - new object[] { new SchemaCreated(), EnrichedSchemaEventType.Created }, - new object[] { new SchemaUpdated(), EnrichedSchemaEventType.Updated }, - new object[] { new SchemaDeleted(), EnrichedSchemaEventType.Deleted }, - new object[] { new SchemaPublished(), EnrichedSchemaEventType.Published }, - new object[] { new SchemaUnpublished(), EnrichedSchemaEventType.Unpublished } - }; - - [Theory] - [MemberData(nameof(TestEvents))] - public async Task Should_enrich_events(SchemaEvent @event, EnrichedSchemaEventType type) - { - var envelope = Envelope.Create(@event).SetEventStreamNumber(12); - - var result = await sut.CreateEnrichedEventAsync(envelope); - - Assert.Equal(type, ((EnrichedSchemaEvent)result).Type); - } - - [Fact] - public void Should_not_trigger_precheck_when_event_type_not_correct() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new AppCreated(), trigger, Guid.NewGuid()); - - Assert.False(result); - }); - } - - [Fact] - public void Should_trigger_precheck_when_event_type_correct() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new SchemaCreated(), trigger, Guid.NewGuid()); - - Assert.True(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_event_type_not_correct() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new EnrichedContentEvent(), trigger); - - Assert.False(result); - }); - } - - [Fact] - public void Should_trigger_check_when_condition_is_empty() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); - - Assert.True(result); - }); - } - - [Fact] - public void Should_trigger_check_when_condition_matchs() - { - TestForCondition("true", trigger => - { - var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); - - Assert.True(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_condition_does_not_matchs() - { - TestForCondition("false", trigger => - { - var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); - - Assert.False(result); - }); - } - - private void TestForCondition(string condition, Action action) - { - var trigger = new SchemaChangedTrigger { Condition = condition }; - - action(trigger); - - if (string.IsNullOrWhiteSpace(condition)) - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) - .MustNotHaveHappened(); - } - else - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) - .MustHaveHappened(); - } - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj deleted file mode 100644 index 3171989f0..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - Exe - netcoreapp2.2 - 2.2.0 - Squidex.Domain.Apps.Entities - 7.3 - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs deleted file mode 100644 index 5ec9b5fce..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.TestHelpers -{ - public static class AExtensions - { - public static ClrQuery Is(this INegatableArgumentConstraintManager that, string query) - { - return that.Matches(x => x.ToString() == query); - } - - public static T[] Is(this INegatableArgumentConstraintManager that, params T[] values) - { - if (values == null) - { - return that.IsNull(); - } - - return that.IsSameSequenceAs(values); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/JsonHelper.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/JsonHelper.cs deleted file mode 100644 index 5980e336e..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/JsonHelper.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Newtonsoft; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.TestHelpers -{ - public static class JsonHelper - { - public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); - - public static IJsonSerializer CreateSerializer(TypeNameRegistry typeNameRegistry = null) - { - var serializerSettings = DefaultSettings(typeNameRegistry); - - return new NewtonsoftJsonSerializer(serializerSettings); - } - - public static JsonSerializerSettings DefaultSettings(TypeNameRegistry typeNameRegistry = null) - { - return new JsonSerializerSettings - { - SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry ?? new TypeNameRegistry()), - - ContractResolver = new ConverterContractResolver( - new ClaimsPrincipalConverter(), - new InstantConverter(), - new EnvelopeHeadersConverter(), - new FilterConverter(), - new JsonValueConverter(), - new LanguageConverter(), - new NamedGuidIdConverter(), - new NamedLongIdConverter(), - new NamedStringIdConverter(), - new PropertyPathConverter(), - new RefTokenConverter(), - new StringEnumConverter()), - - TypeNameHandling = TypeNameHandling.Auto - }; - } - - public static T SerializeAndDeserialize(this T value) - { - return DefaultSerializer.Deserialize>(DefaultSerializer.Serialize(Tuple.Create(value))).Item1; - } - - public static T Deserialize(string value) - { - return DefaultSerializer.Deserialize>($"{{ \"Item1\": \"{value}\" }}").Item1; - } - - public static T Deserialize(object value) - { - return DefaultSerializer.Deserialize>($"{{ \"Item1\": {value} }}").Item1; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs deleted file mode 100644 index 3aa99dc28..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Security.Claims; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Shared; - -namespace Squidex.Domain.Apps.Entities.TestHelpers -{ - public static class Mocks - { - public static IAppEntity App(NamedId appId, params Language[] languages) - { - var config = LanguagesConfig.English; - - foreach (var language in languages) - { - config = config.Set(language); - } - - var app = A.Fake(); - - A.CallTo(() => app.Id).Returns(appId.Id); - A.CallTo(() => app.Name).Returns(appId.Name); - A.CallTo(() => app.LanguagesConfig).Returns(config); - - return app; - } - - public static ISchemaEntity Schema(NamedId appId, NamedId schemaId, Schema schemaDef = null) - { - var schema = A.Fake(); - - A.CallTo(() => schema.Id).Returns(schemaId.Id); - A.CallTo(() => schema.AppId).Returns(appId); - A.CallTo(() => schema.SchemaDef).Returns(schemaDef ?? new Schema(schemaId.Name)); - - return schema; - } - - public static ClaimsPrincipal ApiUser(string role = null) - { - return CreateUser(role, "api"); - } - - public static ClaimsPrincipal FrontendUser(string role = null) - { - return CreateUser(role, DefaultClients.Frontend); - } - - private static ClaimsPrincipal CreateUser(string role, string client) - { - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - - claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, client)); - - if (role != null) - { - claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role)); - } - - return claimsPrincipal; - } - } -} diff --git a/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs b/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs deleted file mode 100644 index 1854b3f52..000000000 --- a/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.AspNetCore.Identity; -using Xunit; - -namespace Squidex.Domain.Users -{ - public class DefaultUserResolverTests - { - private readonly UserManager userManager = A.Fake>(); - private readonly DefaultUserResolver sut; - - public DefaultUserResolverTests() - { - var userFactory = A.Fake(); - - A.CallTo(() => userFactory.IsId(A.That.StartsWith("id"))) - .Returns(true); - - A.CallTo(() => userManager.NormalizeKey(A.Ignored)) - .ReturnsLazily(c => c.GetArgument(0).ToUpperInvariant()); - - sut = new DefaultUserResolver(userManager, userFactory); - } - - [Fact] - public async Task Should_resolve_user_by_email() - { - var (user, claims) = GernerateUser("id1"); - - A.CallTo(() => userManager.FindByEmailAsync(user.Email)) - .Returns(user); - - A.CallTo(() => userManager.GetClaimsAsync(user)) - .Returns(claims); - - var result = await sut.FindByIdOrEmailAsync(user.Email); - - Assert.Equal(user.Id, result.Id); - Assert.Equal(user.Email, result.Email); - - Assert.Equal(claims, result.Claims); - } - - [Fact] - public async Task Should_resolve_user_by_id1() - { - var (user, claims) = GernerateUser("id2"); - - A.CallTo(() => userManager.FindByIdAsync(user.Id)) - .Returns(user); - - A.CallTo(() => userManager.GetClaimsAsync(user)) - .Returns(claims); - - var result = await sut.FindByIdOrEmailAsync(user.Id); - - Assert.Equal(user.Id, result.Id); - Assert.Equal(user.Email, result.Email); - - Assert.Equal(claims, result.Claims); - } - - [Fact] - public async Task Should_query_many_by_email_async() - { - var (user1, claims1) = GernerateUser("id1"); - var (user2, claims2) = GernerateUser("id2"); - - var list = new List { user1, user2 }; - - A.CallTo(() => userManager.Users) - .Returns(list.AsQueryable()); - - A.CallTo(() => userManager.GetClaimsAsync(user2)) - .Returns(claims2); - - var result = await sut.QueryByEmailAsync("2"); - - Assert.Equal(user2.Id, result[0].Id); - Assert.Equal(user2.Email, result[0].Email); - - Assert.Equal(claims2, result[0].Claims); - - A.CallTo(() => userManager.GetClaimsAsync(user1)) - .MustNotHaveHappened(); - } - - private static (IdentityUser, List) GernerateUser(string id) - { - var user = new IdentityUser { Id = id, Email = $"email_{id}", NormalizedEmail = $"EMAIL_{id}" }; - - var claims = new List - { - new Claim($"{id}_a", "1"), - new Claim($"{id}_b", "2") - }; - - return (user, claims); - } - } -} diff --git a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj deleted file mode 100644 index 5eddc1a8c..000000000 --- a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - Exe - netcoreapp2.2 - 2.2.0 - Squidex.Domain.Users - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs deleted file mode 100644 index d27d40792..000000000 --- a/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs +++ /dev/null @@ -1,164 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using Xunit; - -namespace Squidex.Infrastructure.Assets -{ - public abstract class AssetStoreTests where T : IAssetStore - { - private readonly MemoryStream assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 }); - private readonly string fileName = Guid.NewGuid().ToString(); - private readonly string sourceFile = Guid.NewGuid().ToString(); - private readonly Lazy sut; - - protected T Sut - { - get { return sut.Value; } - } - - protected string FileName - { - get { return fileName; } - } - - protected AssetStoreTests() - { - sut = new Lazy(CreateStore); - } - - public abstract T CreateStore(); - - [Fact] - public virtual async Task Should_throw_exception_if_asset_to_download_is_not_found() - { - await Assert.ThrowsAsync(() => Sut.DownloadAsync(fileName, new MemoryStream())); - } - - [Fact] - public async Task Should_throw_exception_if_asset_to_copy_is_not_found() - { - await Assert.ThrowsAsync(() => Sut.CopyAsync(fileName, sourceFile)); - } - - [Fact] - public async Task Should_throw_exception_if_stream_to_download_is_null() - { - await Assert.ThrowsAsync(() => Sut.DownloadAsync("File", null)); - } - - [Fact] - public async Task Should_throw_exception_if_stream_to_upload_is_null() - { - await Assert.ThrowsAsync(() => Sut.UploadAsync("File", null)); - } - - [Fact] - public async Task Should_throw_exception_if_source_file_name_to_copy_is_empty() - { - await CheckEmpty(v => Sut.CopyAsync(v, "Target")); - } - - [Fact] - public async Task Should_throw_exception_if_target_file_name_to_copy_is_empty() - { - await CheckEmpty(v => Sut.CopyAsync("Source", v)); - } - - [Fact] - public async Task Should_throw_exception_if_file_name_to_delete_is_empty() - { - await CheckEmpty(v => Sut.DeleteAsync(v)); - } - - [Fact] - public async Task Should_throw_exception_if_file_name_to_download_is_empty() - { - await CheckEmpty(v => Sut.DownloadAsync(v, new MemoryStream())); - } - - [Fact] - public async Task Should_throw_exception_if_file_name_to_upload_is_empty() - { - await CheckEmpty(v => Sut.UploadAsync(v, new MemoryStream())); - } - - [Fact] - public async Task Should_write_and_read_file() - { - await Sut.UploadAsync(fileName, assetData); - - var readData = new MemoryStream(); - - await Sut.DownloadAsync(fileName, readData); - - Assert.Equal(assetData.ToArray(), readData.ToArray()); - } - - [Fact] - public async Task Should_write_and_read_file_and_overwrite_non_existing() - { - await Sut.UploadAsync(fileName, assetData, true); - - var readData = new MemoryStream(); - - await Sut.DownloadAsync(fileName, readData); - - Assert.Equal(assetData.ToArray(), readData.ToArray()); - } - - [Fact] - public async Task Should_write_and_read_overriding_file() - { - var oldData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5 }); - - await Sut.UploadAsync(fileName, oldData); - await Sut.UploadAsync(fileName, assetData, true); - - var readData = new MemoryStream(); - - await Sut.DownloadAsync(fileName, readData); - - Assert.Equal(assetData.ToArray(), readData.ToArray()); - } - - [Fact] - public async Task Should_throw_exception_when_file_to_write_already_exists() - { - await Sut.UploadAsync(fileName, assetData); - - await Assert.ThrowsAsync(() => Sut.UploadAsync(fileName, assetData)); - } - - [Fact] - public async Task Should_throw_exception_when_target_file_to_copy_to_already_exists() - { - await Sut.UploadAsync(sourceFile, assetData); - await Sut.CopyAsync(sourceFile, fileName); - - await Assert.ThrowsAsync(() => Sut.CopyAsync(sourceFile, fileName)); - } - - [Fact] - public async Task Should_ignore_when_deleting_not_existing_file() - { - await Sut.UploadAsync(sourceFile, assetData); - await Sut.DeleteAsync(sourceFile); - await Sut.DeleteAsync(sourceFile); - } - - private static async Task CheckEmpty(Func action) - { - await Assert.ThrowsAsync(() => action(null)); - await Assert.ThrowsAsync(() => action(string.Empty)); - await Assert.ThrowsAsync(() => action(" ")); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs deleted file mode 100644 index f9f75a47d..000000000 --- a/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using Squidex.Infrastructure.Assets.ImageSharp; -using Xunit; - -namespace Squidex.Infrastructure.Assets -{ - public class ImageSharpAssetThumbnailGeneratorTests - { - private readonly ImageSharpAssetThumbnailGenerator sut = new ImageSharpAssetThumbnailGenerator(); - private readonly MemoryStream target = new MemoryStream(); - - [Fact] - public async Task Should_return_same_image_if_no_size_is_passed_for_thumbnail() - { - var source = GetPng(); - - await sut.CreateThumbnailAsync(source, target); - - Assert.Equal(target.Length, source.Length); - } - - [Fact] - public async Task Should_resize_image_to_target() - { - var source = GetPng(); - - await sut.CreateThumbnailAsync(source, target, 1000, 1000, "resize"); - - Assert.True(target.Length > source.Length); - } - - [Fact] - public async Task Should_change_jpeg_quality_and_write_to_target() - { - var source = GetJpeg(); - - await sut.CreateThumbnailAsync(source, target, quality: 10); - - Assert.True(target.Length < source.Length); - } - - [Fact] - public async Task Should_change_png_quality_and_write_to_target() - { - var source = GetPng(); - - await sut.CreateThumbnailAsync(source, target, quality: 10); - - Assert.True(target.Length < source.Length); - } - - [Fact] - public async Task Should_return_image_information_if_image_is_valid() - { - var source = GetPng(); - - var imageInfo = await sut.GetImageInfoAsync(source); - - Assert.Equal(600, imageInfo.PixelHeight); - Assert.Equal(600, imageInfo.PixelWidth); - } - - [Fact] - public async Task Should_return_null_if_stream_is_not_an_image() - { - var source = new MemoryStream(Convert.FromBase64String("YXNkc2Fk")); - - var imageInfo = await sut.GetImageInfoAsync(source); - - Assert.Null(imageInfo); - } - - private Stream GetPng() - { - return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.png"); - } - - private Stream GetJpeg() - { - return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.jpg"); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs deleted file mode 100644 index 8a2851e74..000000000 --- a/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs +++ /dev/null @@ -1,278 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Xunit; - -namespace Squidex.Infrastructure -{ - public class CollectionExtensionsTests - { - private readonly Dictionary valueDictionary = new Dictionary(); - private readonly Dictionary> listDictionary = new Dictionary>(); - - [Fact] - public void GetOrDefault_should_return_value_if_key_exists() - { - valueDictionary[12] = 34; - - Assert.Equal(34, valueDictionary.GetOrDefault(12)); - } - - [Fact] - public void GetOrDefault_should_return_default_and_not_add_it_if_key_not_exists() - { - Assert.Equal(0, valueDictionary.GetOrDefault(12)); - Assert.False(valueDictionary.ContainsKey(12)); - } - - [Fact] - public void GetOrAddDefault_should_return_value_if_key_exists() - { - valueDictionary[12] = 34; - - Assert.Equal(34, valueDictionary.GetOrAddDefault(12)); - } - - [Fact] - public void GetOrAddDefault_should_return_default_and_add_it_if_key_not_exists() - { - Assert.Equal(0, valueDictionary.GetOrAddDefault(12)); - Assert.Equal(0, valueDictionary[12]); - } - - [Fact] - public void GetOrCreate_should_return_value_if_key_exists() - { - valueDictionary[12] = 34; - - Assert.Equal(34, valueDictionary.GetOrCreate(12, x => 34)); - } - - [Fact] - public void GetOrCreate_should_return_default_but_not_add_it_if_key_not_exists() - { - Assert.Equal(24, valueDictionary.GetOrCreate(12, x => 24)); - Assert.False(valueDictionary.ContainsKey(12)); - } - - [Fact] - public void GetOrAdd_should_return_value_if_key_exists() - { - valueDictionary[12] = 34; - - Assert.Equal(34, valueDictionary.GetOrAdd(12, x => 34)); - } - - [Fact] - public void GetOrAdd_should_return_default_and_add_it_if_key_not_exists() - { - Assert.Equal(24, valueDictionary.GetOrAdd(12, 24)); - Assert.Equal(24, valueDictionary[12]); - } - - [Fact] - public void GetOrAdd_should_return_default_and_add_it_with_fallback_if_key_not_exists() - { - Assert.Equal(24, valueDictionary.GetOrAdd(12, x => 24)); - Assert.Equal(24, valueDictionary[12]); - } - - [Fact] - public void GetOrNew_should_return_value_if_key_exists() - { - var list = new List(); - listDictionary[12] = list; - - Assert.Equal(list, listDictionary.GetOrNew(12)); - } - - [Fact] - public void GetOrNew_should_return_default_but_not_add_it_if_key_not_exists() - { - var list = new List(); - - Assert.Equal(list, listDictionary.GetOrNew(12)); - Assert.False(listDictionary.ContainsKey(12)); - } - - [Fact] - public void GetOrAddNew_should_return_value_if_key_exists() - { - var list = new List(); - listDictionary[12] = list; - - Assert.Equal(list, listDictionary.GetOrAddNew(12)); - } - - [Fact] - public void GetOrAddNew_should_return_default_but_not_add_it_if_key_not_exists() - { - var list = new List(); - - Assert.Equal(list, listDictionary.GetOrAddNew(12)); - Assert.Equal(list, listDictionary[12]); - } - - [Fact] - public void SequentialHashCode_should_ignore_null_values() - { - var collection = new string[] { null, null }; - - Assert.Equal(17, collection.SequentialHashCode()); - } - - [Fact] - public void SequentialHashCode_should_return_same_hash_codes_for_list_with_same_order() - { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 3, 5, 6 }; - - Assert.Equal(collection2.SequentialHashCode(), collection1.SequentialHashCode()); - } - - [Fact] - public void SequentialHashCode_should_return_different_hash_codes_for_list_with_different_items() - { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 3, 4, 1 }; - - Assert.NotEqual(collection2.SequentialHashCode(), collection1.SequentialHashCode()); - } - - [Fact] - public void SequentialHashCode_should_return_different_hash_codes_for_list_with_different_order() - { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 6, 5, 3 }; - - Assert.NotEqual(collection2.SequentialHashCode(), collection1.SequentialHashCode()); - } - - [Fact] - public void OrderedHashCode_should_return_same_hash_codes_for_list_with_same_order() - { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 3, 5, 6 }; - - Assert.Equal(collection2.OrderedHashCode(), collection1.OrderedHashCode()); - } - - [Fact] - public void OrderedHashCode_should_return_different_hash_codes_for_list_with_different_items() - { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 3, 4, 1 }; - - Assert.NotEqual(collection2.OrderedHashCode(), collection1.OrderedHashCode()); - } - - [Fact] - public void OrderedHashCode_should_return_same_hash_codes_for_list_with_different_order() - { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 6, 5, 3 }; - - Assert.Equal(collection2.OrderedHashCode(), collection1.OrderedHashCode()); - } - - [Fact] - public void EqualsDictionary_should_return_true_for_equal_dictionaries() - { - var lhs = new Dictionary - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary - { - [1] = 1, - [2] = 2 - }; - - Assert.True(lhs.EqualsDictionary(rhs)); - } - - [Fact] - public void EqualsDictionary_should_return_false_for_different_sizes() - { - var lhs = new Dictionary - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary - { - [1] = 1 - }; - - Assert.False(lhs.EqualsDictionary(rhs)); - } - - [Fact] - public void EqualsDictionary_should_return_false_for_different_values() - { - var lhs = new Dictionary - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary - { - [1] = 1, - [3] = 3 - }; - - Assert.False(lhs.EqualsDictionary(rhs)); - } - - [Fact] - public void Dictionary_should_return_same_hashcode_for_equal_dictionaries() - { - var lhs = new Dictionary - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary - { - [1] = 1, - [2] = 2 - }; - - Assert.Equal(lhs.DictionaryHashCode(), rhs.DictionaryHashCode()); - } - - [Fact] - public void Dictionary_should_return_different_hashcode_for_different_dictionaries() - { - var lhs = new Dictionary - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary - { - [1] = 1, - [3] = 3 - }; - - Assert.NotEqual(lhs.DictionaryHashCode(), rhs.DictionaryHashCode()); - } - - [Fact] - public void Foreach_should_call_action_foreach_item() - { - var source = new List { 3, 5, 1 }; - var target = new List(); - - source.Foreach(target.Add); - - Assert.Equal(source, target); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs deleted file mode 100644 index c12893015..000000000 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Reflection; -using FakeItEasy; -using Orleans; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Commands -{ - public class DomainObjectGrainFormatterTests - { - private readonly IGrainCallContext context = A.Fake(); - - [Fact] - public void Should_return_fallback_if_no_method_is_defined() - { - A.CallTo(() => context.InterfaceMethod) - .Returns(null); - - var result = DomainObjectGrainFormatter.Format(context); - - Assert.Equal("Unknown", result); - } - - [Fact] - public void Should_return_method_name_if_not_domain_object_method() - { - var methodInfo = A.Fake(); - - A.CallTo(() => methodInfo.Name) - .Returns("Calculate"); - - A.CallTo(() => context.InterfaceMethod) - .Returns(methodInfo); - - var result = DomainObjectGrainFormatter.Format(context); - - Assert.Equal("Calculate", result); - } - - [Fact] - public void Should_return_nice_method_name_if_domain_object_execute() - { - var methodInfo = A.Fake(); - - A.CallTo(() => methodInfo.Name) - .Returns(nameof(IDomainObjectGrain.ExecuteAsync)); - - A.CallTo(() => context.Arguments) - .Returns(new object[] { new MyCommand() }); - - A.CallTo(() => context.InterfaceMethod) - .Returns(methodInfo); - - var result = DomainObjectGrainFormatter.Format(context); - - Assert.Equal("ExecuteAsync(MyCommand)", result); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs deleted file mode 100644 index aaba71d95..000000000 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs +++ /dev/null @@ -1,220 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Commands -{ - public class DomainObjectGrainTests - { - private readonly IStore store = A.Fake>(); - private readonly IPersistence persistence = A.Fake>(); - private readonly Guid id = Guid.NewGuid(); - private readonly MyDomainObject sut; - - public sealed class MyDomainObject : DomainObjectGrain - { - public MyDomainObject(IStore store) - : base(store, A.Dummy()) - { - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - switch (command) - { - case CreateAuto createAuto: - return Create(createAuto, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - }); - - case CreateCustom createCustom: - return CreateReturn(createCustom, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - - return "CREATED"; - }); - - case UpdateAuto updateAuto: - return Update(updateAuto, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - }); - - case UpdateCustom updateCustom: - return UpdateReturn(updateCustom, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - - return "UPDATED"; - }); - } - - return Task.FromResult(null); - } - } - - public DomainObjectGrainTests() - { - A.CallTo(() => store.WithSnapshotsAndEventSourcing(typeof(MyDomainObject), id, A>.Ignored, A.Ignored)) - .Returns(persistence); - - sut = new MyDomainObject(store); - } - - [Fact] - public void Should_instantiate() - { - Assert.Equal(EtagVersion.Empty, sut.Version); - } - - [Fact] - public async Task Should_write_state_and_events_when_created() - { - await SetupEmptyAsync(); - - var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); - - A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 4))) - .MustHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) - .MustHaveHappened(); - - Assert.True(result.Value is EntityCreatedResult); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(4, sut.Snapshot.Value); - Assert.Equal(0, sut.Snapshot.Version); - } - - [Fact] - public async Task Should_write_state_and_events_when_updated() - { - await SetupCreatedAsync(); - - var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); - - A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 8))) - .MustHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) - .MustHaveHappened(); - - Assert.True(result.Value is EntitySavedResult); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(8, sut.Snapshot.Value); - Assert.Equal(1, sut.Snapshot.Version); - } - - [Fact] - public async Task Should_throw_exception_when_already_created() - { - await SetupCreatedAsync(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); - } - - [Fact] - public async Task Should_throw_exception_when_not_created() - { - await SetupEmptyAsync(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); - } - - [Fact] - public async Task Should_return_custom_result_on_create() - { - await SetupEmptyAsync(); - - var result = await sut.ExecuteAsync(C(new CreateCustom())); - - Assert.Equal("CREATED", result.Value); - } - - [Fact] - public async Task Should_return_custom_result_on_update() - { - await SetupCreatedAsync(); - - var result = await sut.ExecuteAsync(C(new UpdateCustom())); - - Assert.Equal("UPDATED", result.Value); - } - - [Fact] - public async Task Should_throw_exception_when_other_verison_expected() - { - await SetupCreatedAsync(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 }))); - } - - [Fact] - public async Task Should_reset_state_when_writing_snapshot_for_create_failed() - { - await SetupEmptyAsync(); - - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) - .Throws(new InvalidOperationException()); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(0, sut.Snapshot.Value); - Assert.Equal(-1, sut.Snapshot.Version); - } - - [Fact] - public async Task Should_reset_state_when_writing_snapshot_for_update_failed() - { - await SetupCreatedAsync(); - - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) - .Throws(new InvalidOperationException()); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(4, sut.Snapshot.Value); - Assert.Equal(0, sut.Snapshot.Version); - } - - private async Task SetupCreatedAsync() - { - await sut.ActivateAsync(id); - - await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); - } - - private static J C(IAggregateCommand command) - { - return command.AsJ(); - } - - private async Task SetupEmptyAsync() - { - await sut.ActivateAsync(id); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs deleted file mode 100644 index 94f56de94..000000000 --- a/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs +++ /dev/null @@ -1,280 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FakeItEasy; -using FluentAssertions; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Commands -{ - public class LogSnapshotDomainObjectGrainTests - { - private readonly IStore store = A.Fake>(); - private readonly ISnapshotStore snapshotStore = A.Fake>(); - private readonly IPersistence persistence = A.Fake(); - private readonly Guid id = Guid.NewGuid(); - private readonly MyLogDomainObject sut; - - public sealed class MyLogDomainObject : LogSnapshotDomainObjectGrain - { - public MyLogDomainObject(IStore store) - : base(store, A.Dummy()) - { - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - switch (command) - { - case CreateAuto createAuto: - return Create(createAuto, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - }); - - case CreateCustom createCustom: - return CreateReturn(createCustom, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - - return "CREATED"; - }); - - case UpdateAuto updateAuto: - return Update(updateAuto, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - }); - - case UpdateCustom updateCustom: - return UpdateReturn(updateCustom, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - - return "UPDATED"; - }); - } - - return Task.FromResult(null); - } - } - - public LogSnapshotDomainObjectGrainTests() - { - A.CallTo(() => store.WithEventSourcing(typeof(MyLogDomainObject), id, A.Ignored)) - .Returns(persistence); - - A.CallTo(() => store.GetSnapshotStore()) - .Returns(snapshotStore); - - sut = new MyLogDomainObject(store); - } - - [Fact] - public async Task Should_get_latestet_version_when_requesting_state_with_any() - { - await SetupUpdatedAsync(); - - var result = sut.GetSnapshot(EtagVersion.Any); - - result.Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); - } - - [Fact] - public async Task Should_get_latestet_version_when_requesting_state_with_auto() - { - await SetupUpdatedAsync(); - - var result = sut.GetSnapshot(EtagVersion.Auto); - - result.Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); - } - - [Fact] - public async Task Should_get_empty_version_when_requesting_state_with_empty_version() - { - await SetupUpdatedAsync(); - - var result = sut.GetSnapshot(EtagVersion.Empty); - - result.Should().BeEquivalentTo(new MyDomainState { Value = 0, Version = -1 }); - } - - [Fact] - public async Task Should_get_specific_version_when_requesting_state_with_specific_version() - { - await SetupUpdatedAsync(); - - sut.GetSnapshot(0).Should().BeEquivalentTo(new MyDomainState { Value = 4, Version = 0 }); - sut.GetSnapshot(1).Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); - } - - [Fact] - public async Task Should_get_null_state_when_requesting_state_with_invalid_version() - { - await SetupUpdatedAsync(); - - Assert.Null(sut.GetSnapshot(-4)); - Assert.Null(sut.GetSnapshot(2)); - } - - [Fact] - public void Should_instantiate() - { - Assert.Equal(EtagVersion.Empty, sut.Version); - } - - [Fact] - public async Task Should_write_state_and_events_when_created() - { - await SetupEmptyAsync(); - - var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); - - A.CallTo(() => snapshotStore.WriteAsync(id, A.That.Matches(x => x.Value == 4), -1, 0)) - .MustHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) - .MustHaveHappened(); - - Assert.True(result.Value is EntityCreatedResult); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(4, sut.Snapshot.Value); - Assert.Equal(0, sut.Snapshot.Version); - } - - [Fact] - public async Task Should_write_state_and_events_when_updated() - { - await SetupCreatedAsync(); - - var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); - - A.CallTo(() => snapshotStore.WriteAsync(id, A.That.Matches(x => x.Value == 8), 0, 1)) - .MustHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) - .MustHaveHappened(); - - Assert.True(result.Value is EntitySavedResult); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(8, sut.Snapshot.Value); - Assert.Equal(1, sut.Snapshot.Version); - } - - [Fact] - public async Task Should_throw_exception_when_already_created() - { - await SetupCreatedAsync(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); - } - - [Fact] - public async Task Should_throw_exception_when_not_created() - { - await SetupEmptyAsync(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); - } - - [Fact] - public async Task Should_return_custom_result_on_create() - { - await SetupEmptyAsync(); - - var result = await sut.ExecuteAsync(C(new CreateCustom())); - - Assert.Equal("CREATED", result.Value); - } - - [Fact] - public async Task Should_return_custom_result_on_update() - { - await SetupCreatedAsync(); - - var result = await sut.ExecuteAsync(C(new UpdateCustom())); - - Assert.Equal("UPDATED", result.Value); - } - - [Fact] - public async Task Should_throw_exception_when_other_verison_expected() - { - await SetupCreatedAsync(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 }))); - } - - [Fact] - public async Task Should_reset_state_when_writing_snapshot_for_create_failed() - { - await SetupEmptyAsync(); - - A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, -1, 0)) - .Throws(new InvalidOperationException()); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(0, sut.Snapshot.Value); - Assert.Equal(-1, sut.Snapshot.Version); - } - - [Fact] - public async Task Should_reset_state_when_writing_snapshot_for_update_failed() - { - await SetupCreatedAsync(); - - A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, 0, 1)) - .Throws(new InvalidOperationException()); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(4, sut.Snapshot.Value); - Assert.Equal(0, sut.Snapshot.Version); - } - - private async Task SetupCreatedAsync() - { - await sut.ActivateAsync(id); - - await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); - } - - private async Task SetupUpdatedAsync() - { - await SetupCreatedAsync(); - - await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); - } - - private async Task SetupEmptyAsync() - { - await sut.ActivateAsync(id); - } - - private static J C(IAggregateCommand command) - { - return command.AsJ(); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs deleted file mode 100644 index 54c7772ce..000000000 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs +++ /dev/null @@ -1,376 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Squidex.Infrastructure.Tasks; -using Xunit; - -namespace Squidex.Infrastructure.EventSourcing -{ - public abstract class EventStoreTests where T : IEventStore - { - private readonly Lazy sut; - private string subscriptionPosition; - - public sealed class EventSubscriber : IEventSubscriber - { - public List Events { get; } = new List(); - - public string LastPosition { get; set; } - - public Task OnErrorAsync(IEventSubscription subscription, Exception exception) - { - throw new NotSupportedException(); - } - - public Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) - { - LastPosition = storedEvent.EventPosition; - - Events.Add(storedEvent); - - return TaskHelper.Done; - } - } - - protected T Sut - { - get { return sut.Value; } - } - - protected abstract int SubscriptionDelayInMs { get; } - - protected EventStoreTests() - { - sut = new Lazy(CreateStore); - } - - public abstract T CreateStore(); - - [Fact] - public async Task Should_throw_exception_for_version_mismatch() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - await Assert.ThrowsAsync(() => Sut.AppendAsync(Guid.NewGuid(), streamName, 0, events)); - } - - [Fact] - public async Task Should_throw_exception_for_version_mismatch_and_update() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - await Sut.AppendAsync(Guid.NewGuid(), streamName, events); - - await Assert.ThrowsAsync(() => Sut.AppendAsync(Guid.NewGuid(), streamName, 0, events)); - } - - [Fact] - public async Task Should_append_events() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - await Sut.AppendAsync(Guid.NewGuid(), streamName, events); - - var readEvents1 = await QueryAsync(streamName); - var readEvents2 = await QueryWithCallbackAsync(streamName); - - var expected = new[] - { - new StoredEvent(streamName, "Position", 0, events[0]), - new StoredEvent(streamName, "Position", 1, events[1]) - }; - - ShouldBeEquivalentTo(readEvents1, expected); - ShouldBeEquivalentTo(readEvents2, expected); - } - - [Fact] - public async Task Should_subscribe_to_events() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - var readEvents = await QueryWithSubscriptionAsync(streamName, async () => - { - await Sut.AppendAsync(Guid.NewGuid(), streamName, events); - }); - - var expected = new[] - { - new StoredEvent(streamName, "Position", 0, events[0]), - new StoredEvent(streamName, "Position", 1, events[1]) - }; - - ShouldBeEquivalentTo(readEvents, expected); - } - - [Fact] - public async Task Should_subscribe_to_next_events() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events1 = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - await QueryWithSubscriptionAsync(streamName, async () => - { - await Sut.AppendAsync(Guid.NewGuid(), streamName, events1); - }); - - var events2 = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - var readEventsFromPosition = await QueryWithSubscriptionAsync(streamName, async () => - { - await Sut.AppendAsync(Guid.NewGuid(), streamName, events2); - }); - - var expectedFromPosition = new[] - { - new StoredEvent(streamName, "Position", 2, events2[0]), - new StoredEvent(streamName, "Position", 3, events2[1]) - }; - - var readEventsFromBeginning = await QueryWithSubscriptionAsync(streamName, fromBeginning: true); - - var expectedFromBeginning = new[] - { - new StoredEvent(streamName, "Position", 0, events1[0]), - new StoredEvent(streamName, "Position", 1, events1[1]), - new StoredEvent(streamName, "Position", 2, events2[0]), - new StoredEvent(streamName, "Position", 3, events2[1]) - }; - - ShouldBeEquivalentTo(readEventsFromPosition, expectedFromPosition); - - ShouldBeEquivalentTo(readEventsFromBeginning, expectedFromBeginning); - } - - [Fact] - public async Task Should_read_events_from_offset() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - await Sut.AppendAsync(Guid.NewGuid(), streamName, events); - - var firstRead = await QueryAsync(streamName); - - var readEvents1 = await QueryAsync(streamName, 1); - var readEvents2 = await QueryWithCallbackAsync(streamName, firstRead[0].EventPosition); - - var expected = new[] - { - new StoredEvent(streamName, "Position", 1, events[1]) - }; - - ShouldBeEquivalentTo(readEvents1, expected); - ShouldBeEquivalentTo(readEvents2, expected); - } - - [Fact] - public async Task Should_delete_stream() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - await Sut.AppendAsync(Guid.NewGuid(), streamName, events); - - await Sut.DeleteStreamAsync(streamName); - - var readEvents = await QueryAsync(streamName); - - Assert.Empty(readEvents); - } - - [Fact] - public async Task Should_query_events_by_property() - { - var keyed1 = new EnvelopeHeaders(); - var keyed2 = new EnvelopeHeaders(); - - keyed1.Add("key", Guid.NewGuid().ToString()); - keyed2.Add("key", Guid.NewGuid().ToString()); - - var streamName1 = $"test-{Guid.NewGuid()}"; - var streamName2 = $"test-{Guid.NewGuid()}"; - - var events1 = new[] - { - new EventData("Type1", keyed1, "1"), - new EventData("Type2", keyed2, "2") - }; - - var events2 = new[] - { - new EventData("Type3", keyed2, "3"), - new EventData("Type4", keyed1, "4") - }; - - await Sut.CreateIndexAsync("key"); - - await Sut.AppendAsync(Guid.NewGuid(), streamName1, events1); - await Sut.AppendAsync(Guid.NewGuid(), streamName2, events2); - - var readEvents = await QueryWithFilterAsync("key", keyed2["key"].ToString()); - - var expected = new[] - { - new StoredEvent(streamName1, "Position", 1, events1[1]), - new StoredEvent(streamName2, "Position", 0, events2[0]) - }; - - ShouldBeEquivalentTo(readEvents, expected); - } - - private Task> QueryAsync(string streamName, long position = EtagVersion.Any) - { - return Sut.QueryAsync(streamName, position); - } - - private async Task> QueryWithFilterAsync(string property, object value) - { - using (var cts = new CancellationTokenSource(30000)) - { - while (!cts.IsCancellationRequested) - { - var readEvents = new List(); - - await Sut.QueryAsync(x => { readEvents.Add(x); return TaskHelper.Done; }, property, value, null, cts.Token); - - await Task.Delay(500, cts.Token); - - if (readEvents.Count > 0) - { - return readEvents; - } - } - - cts.Token.ThrowIfCancellationRequested(); - - return null; - } - } - - private async Task> QueryWithCallbackAsync(string streamFilter = null, string position = null) - { - using (var cts = new CancellationTokenSource(30000)) - { - while (!cts.IsCancellationRequested) - { - var readEvents = new List(); - - await Sut.QueryAsync(x => { readEvents.Add(x); return TaskHelper.Done; }, streamFilter, position, cts.Token); - - await Task.Delay(500, cts.Token); - - if (readEvents.Count > 0) - { - return readEvents; - } - } - - cts.Token.ThrowIfCancellationRequested(); - - return null; - } - } - - private async Task> QueryWithSubscriptionAsync(string streamFilter, Func action = null, bool fromBeginning = false) - { - var subscriber = new EventSubscriber(); - - IEventSubscription subscription = null; - try - { - subscription = Sut.CreateSubscription(subscriber, streamFilter, fromBeginning ? null : subscriptionPosition); - - if (action != null) - { - await action(); - } - - using (var cts = new CancellationTokenSource(30000)) - { - while (!cts.IsCancellationRequested) - { - subscription.WakeUp(); - - await Task.Delay(500, cts.Token); - - if (subscriber.Events.Count > 0) - { - subscriptionPosition = subscriber.LastPosition; - - return subscriber.Events; - } - } - - cts.Token.ThrowIfCancellationRequested(); - - return null; - } - } - finally - { - await subscription.StopAsync(); - } - } - - private static void ShouldBeEquivalentTo(IEnumerable actual, params StoredEvent[] expected) - { - var actualArray = actual.Select(x => new StoredEvent(x.StreamName, "Position", x.EventStreamNumber, x.Data)).ToArray(); - - actualArray.Should().BeEquivalentTo(expected); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs deleted file mode 100644 index 71f4a6c56..000000000 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs +++ /dev/null @@ -1,409 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using FluentAssertions; -using Orleans.Concurrency; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public class EventConsumerGrainTests - { - public sealed class MyEventConsumerGrain : EventConsumerGrain - { - public MyEventConsumerGrain( - EventConsumerFactory eventConsumerFactory, - IGrainState state, - IEventStore eventStore, - IEventDataFormatter eventDataFormatter, - ISemanticLog log) - : base(eventConsumerFactory, state, eventStore, eventDataFormatter, log) - { - } - - protected override IEventConsumerGrain GetSelf() - { - return this; - } - - protected override IEventSubscription CreateSubscription(IEventStore store, IEventSubscriber subscriber, string streamFilter, string position) - { - return store.CreateSubscription(subscriber, streamFilter, position); - } - } - - private readonly IGrainState grainState = A.Fake>(); - private readonly IEventConsumer eventConsumer = A.Fake(); - private readonly IEventStore eventStore = A.Fake(); - private readonly IEventSubscription eventSubscription = A.Fake(); - private readonly ISemanticLog log = A.Fake(); - private readonly IEventDataFormatter formatter = A.Fake(); - private readonly EventData eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); - private readonly Envelope envelope = new Envelope(new MyEvent()); - private readonly EventConsumerGrain sut; - private readonly string consumerName; - private readonly string initialPosition = Guid.NewGuid().ToString(); - - public EventConsumerGrainTests() - { - grainState.Value.Position = initialPosition; - - consumerName = eventConsumer.GetType().Name; - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) - .Returns(eventSubscription); - - A.CallTo(() => eventConsumer.Name) - .Returns(consumerName); - - A.CallTo(() => eventConsumer.Handles(A.Ignored)) - .Returns(true); - - A.CallTo(() => formatter.Parse(eventData, null)) - .Returns(envelope); - - sut = new MyEventConsumerGrain( - x => eventConsumer, - grainState, - eventStore, - formatter, - log); - } - - [Fact] - public async Task Should_not_subscribe_to_event_store_when_stopped_in_db() - { - grainState.Value = grainState.Value.Stopped(); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_subscribe_to_event_store_when_not_found_in_db() - { - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_subscribe_to_event_store_when_not_stopped_in_db() - { - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_stop_subscription_when_stopped() - { - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - await sut.StopAsync(); - await sut.StopAsync(); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_reset_consumer_when_resetting() - { - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - await sut.StopAsync(); - await sut.ResetAsync(); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = null, Error = null }); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(2, Times.Exactly); - - A.CallTo(() => eventConsumer.ClearAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, grainState.Value.Position)) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, null)) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_invoke_and_update_position_when_event_received() - { - var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnEventAsync(eventSubscription, @event); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventConsumer.On(envelope)) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_not_invoke_but_update_position_when_consumer_does_not_want_to_handle() - { - var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - A.CallTo(() => eventConsumer.Handles(@event)) - .Returns(false); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnEventAsync(eventSubscription, @event); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventConsumer.On(envelope)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_ignore_old_events() - { - A.CallTo(() => formatter.Parse(eventData, null)) - .Throws(new TypeNameNotFoundException()); - - var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnEventAsync(eventSubscription, @event); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventConsumer.On(envelope)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_invoke_and_update_position_when_event_is_from_another_subscription() - { - var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnEventAsync(A.Fake(), @event); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); - - A.CallTo(() => eventConsumer.On(envelope)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_stop_if_consumer_failed() - { - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - var ex = new InvalidOperationException(); - - await OnErrorAsync(eventSubscription, ex); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_not_make_error_handling_when_exception_is_from_another_subscription() - { - var ex = new InvalidOperationException(); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnErrorAsync(A.Fake(), ex); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); - - A.CallTo(() => grainState.WriteAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_wakeup_when_already_subscribed() - { - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - await sut.ActivateAsync(); - - A.CallTo(() => eventSubscription.WakeUp()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_stop_if_resetting_failed() - { - var ex = new InvalidOperationException(); - - A.CallTo(() => eventConsumer.ClearAsync()) - .Throws(ex); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - await sut.ResetAsync(); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_stop_if_handling_failed() - { - var ex = new InvalidOperationException(); - - A.CallTo(() => eventConsumer.On(envelope)) - .Throws(ex); - - var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnEventAsync(eventSubscription, @event); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); - - A.CallTo(() => eventConsumer.On(envelope)) - .MustHaveHappened(); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_stop_if_deserialization_failed() - { - var ex = new InvalidOperationException(); - - A.CallTo(() => formatter.Parse(eventData, null)) - .Throws(ex); - - var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnEventAsync(eventSubscription, @event); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); - - A.CallTo(() => eventConsumer.On(envelope)) - .MustNotHaveHappened(); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_start_after_stop_when_handling_failed() - { - var exception = new InvalidOperationException(); - - A.CallTo(() => eventConsumer.On(envelope)) - .Throws(exception); - - var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnEventAsync(eventSubscription, @event); - - await sut.StopAsync(); - await sut.StartAsync(); - await sut.StartAsync(); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); - - A.CallTo(() => eventConsumer.On(envelope)) - .MustHaveHappened(); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(2, Times.Exactly); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) - .MustHaveHappened(2, Times.Exactly); - } - - private Task OnErrorAsync(IEventSubscription subscriber, Exception ex) - { - return sut.OnErrorAsync(subscriber.AsImmutable(), ex.AsImmutable()); - } - - private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev) - { - return sut.OnEventAsync(subscriber.AsImmutable(), ev.AsImmutable()); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs deleted file mode 100644 index acf9c6a7a..000000000 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs +++ /dev/null @@ -1,186 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using FluentAssertions; -using Orleans; -using Orleans.Concurrency; -using Orleans.Core; -using Orleans.Runtime; -using Xunit; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public class EventConsumerManagerGrainTests - { - public class MyEventConsumerManagerGrain : EventConsumerManagerGrain - { - public MyEventConsumerManagerGrain( - IEnumerable eventConsumers, - IGrainIdentity identity, - IGrainRuntime runtime) - : base(eventConsumers, identity, runtime) - { - } - } - - private readonly IEventConsumer consumerA = A.Fake(); - private readonly IEventConsumer consumerB = A.Fake(); - private readonly IEventConsumerGrain grainA = A.Fake(); - private readonly IEventConsumerGrain grainB = A.Fake(); - private readonly MyEventConsumerManagerGrain sut; - - public EventConsumerManagerGrainTests() - { - var grainRuntime = A.Fake(); - var grainFactory = A.Fake(); - - A.CallTo(() => grainFactory.GetGrain("a", null)).Returns(grainA); - A.CallTo(() => grainFactory.GetGrain("b", null)).Returns(grainB); - A.CallTo(() => grainRuntime.GrainFactory).Returns(grainFactory); - - A.CallTo(() => consumerA.Name).Returns("a"); - A.CallTo(() => consumerA.EventsFilter).Returns("^a-"); - - A.CallTo(() => consumerB.Name).Returns("b"); - A.CallTo(() => consumerB.EventsFilter).Returns("^b-"); - - sut = new MyEventConsumerManagerGrain(new[] { consumerA, consumerB }, A.Fake(), grainRuntime); - } - - [Fact] - public async Task Should_not_activate_all_grains_on_activate() - { - await sut.OnActivateAsync(); - - A.CallTo(() => grainA.ActivateAsync()) - .MustNotHaveHappened(); - - A.CallTo(() => grainB.ActivateAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_activate_all_grains_on_reminder() - { - await sut.ReceiveReminder(null, default); - - A.CallTo(() => grainA.ActivateAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.ActivateAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_activate_all_grains_on_wakeup_with_null() - { - await sut.ActivateAsync(null); - - A.CallTo(() => grainA.ActivateAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.ActivateAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_activate_matching_grains_when_stream_name_defined() - { - await sut.ActivateAsync("a-123"); - - A.CallTo(() => grainA.ActivateAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.ActivateAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_start_all_grains() - { - await sut.StartAllAsync(); - - A.CallTo(() => grainA.StartAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.StartAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_start_matching_grain() - { - await sut.StartAsync("a"); - - A.CallTo(() => grainA.StartAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.StartAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_stop_all_grains() - { - await sut.StopAllAsync(); - - A.CallTo(() => grainA.StopAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.StopAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_stop_matching_grain() - { - await sut.StopAsync("b"); - - A.CallTo(() => grainA.StopAsync()) - .MustNotHaveHappened(); - - A.CallTo(() => grainB.StopAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_reset_matching_grain() - { - await sut.ResetAsync("b"); - - A.CallTo(() => grainA.ResetAsync()) - .MustNotHaveHappened(); - - A.CallTo(() => grainB.ResetAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_fetch_infos_from_all_grains() - { - A.CallTo(() => grainA.GetStateAsync()) - .Returns(new Immutable( - new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" })); - - A.CallTo(() => grainB.GetStateAsync()) - .Returns(new Immutable( - new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" })); - - var infos = await sut.GetConsumersAsync(); - - infos.Value.Should().BeEquivalentTo( - new List - { - new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" }, - new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" } - }); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs deleted file mode 100644 index 95d3759d0..000000000 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Xunit; - -namespace Squidex.Infrastructure.EventSourcing -{ - public class RetrySubscriptionTests - { - private readonly IEventStore eventStore = A.Fake(); - private readonly IEventSubscriber eventSubscriber = A.Fake(); - private readonly IEventSubscription eventSubscription = A.Fake(); - private readonly IEventSubscriber sutSubscriber; - private readonly RetrySubscription sut; - private readonly string streamFilter = Guid.NewGuid().ToString(); - - public RetrySubscriptionTests() - { - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)).Returns(eventSubscription); - - sut = new RetrySubscription(eventStore, eventSubscriber, streamFilter, null) { ReconnectWaitMs = 50 }; - - sutSubscriber = sut; - } - - [Fact] - public async Task Should_subscribe_after_constructor() - { - await sut.StopAsync(); - - A.CallTo(() => eventStore.CreateSubscription(sut, streamFilter, null)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_reopen_subscription_once_when_exception_is_retrieved() - { - await OnErrorAsync(eventSubscription, new InvalidOperationException()); - - await Task.Delay(1000); - - await sut.StopAsync(); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(2, Times.Exactly); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) - .MustHaveHappened(2, Times.Exactly); - - A.CallTo(() => eventSubscriber.OnErrorAsync(A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_forward_error_from_inner_subscription_when_failed_often() - { - var ex = new InvalidOperationException(); - - await OnErrorAsync(eventSubscription, ex); - await OnErrorAsync(null, ex); - await OnErrorAsync(null, ex); - await OnErrorAsync(null, ex); - await OnErrorAsync(null, ex); - await OnErrorAsync(null, ex); - await sut.StopAsync(); - - A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_forward_error_when_exception_is_from_another_subscription() - { - var ex = new InvalidOperationException(); - - await OnErrorAsync(A.Fake(), ex); - await sut.StopAsync(); - - A.CallTo(() => eventSubscriber.OnErrorAsync(A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_forward_event_from_inner_subscription() - { - var ev = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); - - await OnEventAsync(eventSubscription, ev); - await sut.StopAsync(); - - A.CallTo(() => eventSubscriber.OnEventAsync(sut, ev)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_forward_event_when_message_is_from_another_subscription() - { - var ev = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); - - await OnEventAsync(A.Fake(), ev); - await sut.StopAsync(); - - A.CallTo(() => eventSubscriber.OnEventAsync(A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - private Task OnErrorAsync(IEventSubscription subscriber, Exception ex) - { - return sutSubscriber.OnErrorAsync(subscriber, ex); - } - - private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev) - { - return sutSubscriber.OnEventAsync(subscriber, ev); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/GuardTests.cs b/tests/Squidex.Infrastructure.Tests/GuardTests.cs deleted file mode 100644 index caabb05dc..000000000 --- a/tests/Squidex.Infrastructure.Tests/GuardTests.cs +++ /dev/null @@ -1,367 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Infrastructure -{ - public class GuardTests - { - private sealed class MyValidatableValid : IValidatable - { - public void Validate(IList errors) - { - } - } - - private sealed class MyValidatableInvalid : IValidatable - { - public void Validate(IList errors) - { - errors.Add(new ValidationError("error.", "error")); - } - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - public void NotNullOrEmpty_should_throw_for_empy_strings(string invalidString) - { - Assert.Throws(() => Guard.NotNullOrEmpty(invalidString, "parameter")); - } - - [Fact] - public void NotNullOrEmpty_should_throw_for_null_string() - { - Assert.Throws(() => Guard.NotNullOrEmpty(null, "parameter")); - } - - [Fact] - public void NotNullOrEmpty_should_do_nothing_for_vaid_string() - { - Guard.NotNullOrEmpty("value", "parameter"); - } - - [Fact] - public void NotNull_should_throw_for_null_value() - { - Assert.Throws(() => Guard.NotNull(null, "parameter")); - } - - [Fact] - public void NotNull_should_do_nothing_for_valid_value() - { - Guard.NotNull("value", "parameter"); - } - - [Fact] - public void Enum_should_throw_for_invalid_enum() - { - Assert.Throws(() => Guard.Enum((DateTimeKind)13, "Parameter")); - } - - [Fact] - public void Enum_should_do_nothing_for_valid_enum() - { - Guard.Enum(DateTimeKind.Local, "Parameter"); - } - - [Fact] - public void NotEmpty_should_throw_for_empty_guid() - { - Assert.Throws(() => Guard.NotEmpty(Guid.Empty, "parameter")); - } - - [Fact] - public void NotEmpty_should_do_nothing_for_valid_guid() - { - Guard.NotEmpty(Guid.NewGuid(), "parameter"); - } - - [Fact] - public void HasType_should_throw_for_other_type() - { - Assert.Throws(() => Guard.HasType("value", "parameter")); - } - - [Fact] - public void HasType_should_do_nothing_for_null_value() - { - Guard.HasType(null, "parameter"); - } - - [Fact] - public void HasType_should_do_nothing_for_correct_type() - { - Guard.HasType(123, "parameter"); - } - - [Fact] - public void HasType_nongeneric_should_throw_for_other_type() - { - Assert.Throws(() => Guard.HasType("value", typeof(int), "parameter")); - } - - [Fact] - public void HasType_nongeneric_should_do_nothing_for_null_value() - { - Guard.HasType(null, typeof(int), "parameter"); - } - - [Fact] - public void HasType_nongeneric_should_do_nothing_for_correct_type() - { - Guard.HasType(123, typeof(int), "parameter"); - } - - [Fact] - public void HasType_nongeneric_should_do_nothing_for_null_type() - { - Guard.HasType(123, null, "parameter"); - } - - [Fact] - public void NotDefault_should_throw_for_default_values() - { - Assert.Throws(() => Guard.NotDefault(Guid.Empty, "parameter")); - Assert.Throws(() => Guard.NotDefault(0, "parameter")); - Assert.Throws(() => Guard.NotDefault((string)null, "parameter")); - Assert.Throws(() => Guard.NotDefault(false, "parameter")); - } - - [Fact] - public void NotDefault_should_do_nothing_for_non_default_value() - { - Guard.NotDefault(Guid.NewGuid(), "parameter"); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(" Not a Slug ")] - [InlineData(" not--a--slug ")] - [InlineData(" not-a-slug ")] - [InlineData("-not-a-slug-")] - [InlineData("not$-a-slug")] - [InlineData("not-a-Slug")] - public void ValidSlug_should_throw_for_invalid_slugs(string slug) - { - Assert.Throws(() => Guard.ValidSlug(slug, "parameter")); - } - - [Theory] - [InlineData("slug")] - [InlineData("slug23")] - [InlineData("other-slug")] - [InlineData("just-another-slug")] - public void ValidSlug_should_do_nothing_for_valid_slugs(string slug) - { - Guard.ValidSlug(slug, "parameter"); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(" Not a Property ")] - [InlineData(" not--a--property ")] - [InlineData(" not-a-property ")] - [InlineData("-not-a-property-")] - [InlineData("not$-a-property")] - public void ValidPropertyName_should_throw_for_invalid_slugs(string slug) - { - Assert.Throws(() => Guard.ValidPropertyName(slug, "property")); - } - - [Theory] - [InlineData("property")] - [InlineData("property23")] - [InlineData("other-property")] - [InlineData("other-Property")] - [InlineData("otherProperty")] - [InlineData("just-another-property")] - [InlineData("just-Another-Property")] - [InlineData("justAnotherProperty")] - public void ValidPropertyName_should_do_nothing_for_valid_slugs(string property) - { - Guard.ValidPropertyName(property, "parameter"); - } - - [Theory] - [InlineData(double.PositiveInfinity)] - [InlineData(double.NegativeInfinity)] - [InlineData(double.NaN)] - public void ValidNumber_should_throw_for_invalid_doubles(double value) - { - Assert.Throws(() => Guard.ValidNumber(value, "parameter")); - } - - [Theory] - [InlineData(0d)] - [InlineData(-1000d)] - [InlineData(1000d)] - public void ValidNumber_do_nothing_for_valid_double(double value) - { - Guard.ValidNumber(value, "parameter"); - } - - [Theory] - [InlineData(float.PositiveInfinity)] - [InlineData(float.NegativeInfinity)] - [InlineData(float.NaN)] - public void ValidNumber_should_throw_for_invalid_float(float value) - { - Assert.Throws(() => Guard.ValidNumber(value, "parameter")); - } - - [Theory] - [InlineData(0f)] - [InlineData(-1000f)] - [InlineData(1000f)] - public void ValidNumber_do_nothing_for_valid_float(float value) - { - Guard.ValidNumber(value, "parameter"); - } - - [Theory] - [InlineData(4)] - [InlineData(104)] - public void Between_should_throw_for_values_outside_of_range(int value) - { - Assert.Throws(() => Guard.Between(value, 10, 100, "parameter")); - } - - [Theory] - [InlineData(10)] - [InlineData(55)] - [InlineData(100)] - public void Between_should_do_nothing_for_values_in_range(int value) - { - Guard.Between(value, 10, 100, "parameter"); - } - - [Theory] - [InlineData(0)] - [InlineData(100)] - public void GreaterThan_should_throw_for_smaller_values(int value) - { - Assert.Throws(() => Guard.GreaterThan(value, 100, "parameter")); - } - - [Theory] - [InlineData(101)] - [InlineData(200)] - public void GreaterThan_should_do_nothing_for_greater_values(int value) - { - Guard.GreaterThan(value, 100, "parameter"); - } - - [Theory] - [InlineData(0)] - [InlineData(99)] - public void GreaterEquals_should_throw_for_smaller_values(int value) - { - Assert.Throws(() => Guard.GreaterEquals(value, 100, "parameter")); - } - - [Theory] - [InlineData(100)] - [InlineData(200)] - public void GreaterEquals_should_do_nothing_for_greater_values(int value) - { - Guard.GreaterEquals(value, 100, "parameter"); - } - - [Theory] - [InlineData(1000)] - [InlineData(100)] - public void LessThan_should_throw_for_greater_values(int value) - { - Assert.Throws(() => Guard.LessThan(value, 100, "parameter")); - } - - [Theory] - [InlineData(99)] - [InlineData(50)] - public void LessThan_should_do_nothing_for_smaller_values(int value) - { - Guard.LessThan(value, 100, "parameter"); - } - - [Theory] - [InlineData(1000)] - [InlineData(101)] - public void LessEquals_should_throw_for_greater_values(int value) - { - Assert.Throws(() => Guard.LessEquals(value, 100, "parameter")); - } - - [Theory] - [InlineData(100)] - [InlineData(50)] - public void LessEquals_should_do_nothing_for_smaller_values(int value) - { - Guard.LessEquals(value, 100, "parameter"); - } - - [Fact] - public void NotEmpty_should_throw_for_empty_collection() - { - Assert.Throws(() => Guard.NotEmpty(new int[0], "parameter")); - } - - [Fact] - public void NotEmpty_should_throw_for_null_collection() - { - Assert.Throws(() => Guard.NotEmpty((int[])null, "parameter")); - } - - [Fact] - public void NotEmpty_should_do_nothing_for_value_collection() - { - Guard.NotEmpty(new[] { 1, 2, 3 }, "parameter"); - } - - [Fact] - public void ValidFileName_should_throw_for_invalid_file_name() - { - Assert.Throws(() => Guard.ValidFileName("File/Name", "Parameter")); - } - - [Fact] - public void ValidFileName_should_throw_for_null_file_name() - { - Assert.Throws(() => Guard.ValidFileName(null, "Parameter")); - } - - [Fact] - public void ValidFileName_should_do_nothing_for_valid_file_name() - { - Guard.ValidFileName("FileName", "Parameter"); - } - - [Fact] - public void Valid_should_throw_exception_if_null() - { - Assert.Throws(() => Guard.Valid(null, "Parameter", () => "Message")); - } - - [Fact] - public void Valid_should_throw_exception_if_invalid() - { - Assert.Throws(() => Guard.Valid(new MyValidatableInvalid(), "Parameter", () => "Message")); - } - - [Fact] - public void Valid_should_do_nothing_if_valid() - { - Guard.Valid(new MyValidatableValid(), "Parameter", () => "Message"); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs b/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs deleted file mode 100644 index e84f11584..000000000 --- a/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using Xunit; - -#pragma warning disable SA1122 // Use string.Empty for empty strings - -namespace Squidex.Infrastructure.Http -{ - public class DumpFormatterTests - { - [Fact] - public void Should_format_dump_without_response() - { - var httpRequest = CreateRequest(); - - var dump = DumpFormatter.BuildDump(httpRequest, null, null, null, TimeSpan.FromMinutes(1), true); - - var expected = CreateExpectedDump( - "Request:", - "POST: https://cloud.squidex.io/ HTTP/2.0", - "User-Agent: Squidex/1.0", - "Accept-Language: de; en", - "Accept-Encoding: UTF-8", - "", - "", - "Response:", - "Timeout after 00:01:00"); - - Assert.Equal(expected, dump); - } - - [Fact] - public void Should_format_dump_without_content() - { - var httpRequest = CreateRequest(); - var httpResponse = CreateResponse(); - - var dump = DumpFormatter.BuildDump(httpRequest, httpResponse, null, null, TimeSpan.FromMinutes(1), false); - - var expected = CreateExpectedDump( - "Request:", - "POST: https://cloud.squidex.io/ HTTP/2.0", - "User-Agent: Squidex/1.0", - "Accept-Language: de; en", - "Accept-Encoding: UTF-8", - "", - "", - "Response:", - "HTTP/1.1 200 OK", - "Transfer-Encoding: UTF-8", - "Trailer: Expires", - "", - "Elapsed: 00:01:00"); - - Assert.Equal(expected, dump); - } - - [Fact] - public void Should_format_dump_with_content_without_timeout() - { - var httpRequest = CreateRequest(new StringContent("Hello Squidex", Encoding.UTF8, "text/plain")); - var httpResponse = CreateResponse(new StringContent("Hello Back", Encoding.UTF8, "text/plain")); - - var dump = DumpFormatter.BuildDump(httpRequest, httpResponse, "Hello Squidex", "Hello Back", TimeSpan.FromMinutes(1), false); - - var expected = CreateExpectedDump( - "Request:", - "POST: https://cloud.squidex.io/ HTTP/2.0", - "User-Agent: Squidex/1.0", - "Accept-Language: de; en", - "Accept-Encoding: UTF-8", - "Content-Type: text/plain; charset=utf-8", - "", - "Hello Squidex", - "", - "", - "Response:", - "HTTP/1.1 200 OK", - "Transfer-Encoding: UTF-8", - "Trailer: Expires", - "Content-Type: text/plain; charset=utf-8", - "", - "Hello Back", - "", - "Elapsed: 00:01:00"); - - Assert.Equal(expected, dump); - } - - private static HttpRequestMessage CreateRequest(HttpContent content = null) - { - var request = new HttpRequestMessage(HttpMethod.Post, new Uri("https://cloud.squidex.io")); - - request.Headers.UserAgent.Add(new ProductInfoHeaderValue("Squidex", "1.0")); - request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("de")); - request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("en")); - request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("UTF-8")); - - request.Content = content; - - return request; - } - - private static HttpResponseMessage CreateResponse(HttpContent content = null) - { - var response = new HttpResponseMessage(HttpStatusCode.OK); - - response.Headers.TransferEncoding.Add(new TransferCodingHeaderValue("UTF-8")); - response.Headers.Trailer.Add("Expires"); - - response.Content = content; - - return response; - } - - private static string CreateExpectedDump(params string[] input) - { - return string.Join(Environment.NewLine, input) + Environment.NewLine; - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs b/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs deleted file mode 100644 index cf1e98f0c..000000000 --- a/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; -using System.Security.Claims; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Json -{ - public class ClaimsPrincipalConverterTests - { - [Fact] - public void Should_serialize_and_deserialize() - { - var value = new ClaimsPrincipal( - new[] - { - new ClaimsIdentity( - new[] - { - new Claim("email", "me@email.com"), - new Claim("username", "me@email.com") - }, - "Cookie"), - new ClaimsIdentity( - new[] - { - new Claim("user_id", "12345"), - new Claim("login", "me") - }, - "Google") - }); - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value.Identities.ElementAt(0).AuthenticationType, serialized.Identities.ElementAt(0).AuthenticationType); - Assert.Equal(value.Identities.ElementAt(1).AuthenticationType, serialized.Identities.ElementAt(1).AuthenticationType); - } - - [Fact] - public void Should_serialize_and_deserialize_null_principal() - { - ClaimsPrincipal value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Null(serialized); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs b/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs deleted file mode 100644 index 5ad034901..000000000 --- a/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs +++ /dev/null @@ -1,357 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using NodaTime; -using Xunit; - -namespace Squidex.Infrastructure.Json.Objects -{ - public class JsonObjectTests - { - [Fact] - public void Should_make_correct_object_equal_comparisons() - { - var obj_count1_key1_val1_a = JsonValue.Object().Add("key1", 1); - var obj_count1_key1_val1_b = JsonValue.Object().Add("key1", 1); - - var obj_count1_key1_val2 = JsonValue.Object().Add("key1", 2); - var obj_count1_key2_val1 = JsonValue.Object().Add("key2", 1); - var obj_count2_key1_val1 = JsonValue.Object().Add("key1", 1).Add("key2", 2); - - var number = JsonValue.Create(1); - - Assert.Equal(obj_count1_key1_val1_a, obj_count1_key1_val1_b); - Assert.Equal(obj_count1_key1_val1_a.GetHashCode(), obj_count1_key1_val1_b.GetHashCode()); - Assert.True(obj_count1_key1_val1_a.Equals((object)obj_count1_key1_val1_b)); - - Assert.NotEqual(obj_count1_key1_val1_a, obj_count1_key1_val2); - Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), obj_count1_key1_val2.GetHashCode()); - Assert.False(obj_count1_key1_val1_a.Equals((object)obj_count1_key1_val2)); - - Assert.NotEqual(obj_count1_key1_val1_a, obj_count1_key2_val1); - Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), obj_count1_key2_val1.GetHashCode()); - Assert.False(obj_count1_key1_val1_a.Equals((object)obj_count1_key2_val1)); - - Assert.NotEqual(obj_count1_key1_val1_a, obj_count2_key1_val1); - Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), obj_count2_key1_val1.GetHashCode()); - Assert.False(obj_count1_key1_val1_a.Equals((object)obj_count2_key1_val1)); - - Assert.NotEqual(obj_count1_key1_val1_a, number); - Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), number.GetHashCode()); - Assert.False(obj_count1_key1_val1_a.Equals((object)number)); - } - - [Fact] - public void Should_make_correct_array_equal_comparisons() - { - var array_count1_val1_a = JsonValue.Array(1); - var array_count1_val1_b = JsonValue.Array(1); - - var array_count1_val2 = JsonValue.Array(2); - var array_count2_val1 = JsonValue.Array(1, 2); - - var number = JsonValue.Create(1); - - Assert.Equal(array_count1_val1_a, array_count1_val1_b); - Assert.Equal(array_count1_val1_a.GetHashCode(), array_count1_val1_b.GetHashCode()); - Assert.True(array_count1_val1_a.Equals((object)array_count1_val1_b)); - - Assert.NotEqual(array_count1_val1_a, array_count1_val2); - Assert.NotEqual(array_count1_val1_a.GetHashCode(), array_count1_val2.GetHashCode()); - Assert.False(array_count1_val1_a.Equals((object)array_count1_val2)); - - Assert.NotEqual(array_count1_val1_a, array_count2_val1); - Assert.NotEqual(array_count1_val1_a.GetHashCode(), array_count2_val1.GetHashCode()); - Assert.False(array_count1_val1_a.Equals((object)array_count2_val1)); - - Assert.NotEqual(array_count1_val1_a, number); - Assert.NotEqual(array_count1_val1_a.GetHashCode(), number.GetHashCode()); - Assert.False(array_count1_val1_a.Equals((object)number)); - } - - [Fact] - public void Should_make_correct_array_scalar_comparisons() - { - var number_val1_a = JsonValue.Create(1); - var number_val1_b = JsonValue.Create(1); - - var number_val2 = JsonValue.Create(2); - - var boolean = JsonValue.True; - - Assert.Equal(number_val1_a, number_val1_b); - Assert.Equal(number_val1_a.GetHashCode(), number_val1_b.GetHashCode()); - Assert.True(number_val1_a.Equals((object)number_val1_b)); - - Assert.NotEqual(number_val1_a, number_val2); - Assert.NotEqual(number_val1_a.GetHashCode(), number_val2.GetHashCode()); - Assert.False(number_val1_a.Equals((object)number_val2)); - - Assert.NotEqual(number_val1_a, boolean); - Assert.NotEqual(number_val1_a.GetHashCode(), boolean.GetHashCode()); - Assert.False(number_val1_a.Equals((object)boolean)); - } - - [Fact] - public void Should_make_correct_null_comparisons() - { - var null_a = JsonValue.Null; - var null_b = JsonValue.Null; - - var boolean = JsonValue.True; - - Assert.Equal(null_a, null_b); - Assert.Equal(null_a.GetHashCode(), null_b.GetHashCode()); - Assert.True(null_a.Equals((object)null_b)); - - Assert.NotEqual(null_a, boolean); - Assert.NotEqual(null_a.GetHashCode(), boolean.GetHashCode()); - Assert.False(null_a.Equals((object)boolean)); - } - - [Fact] - public void Should_cache_null() - { - Assert.Same(JsonValue.Null, JsonValue.Create((string)null)); - Assert.Same(JsonValue.Null, JsonValue.Create((bool?)null)); - Assert.Same(JsonValue.Null, JsonValue.Create((double?)null)); - Assert.Same(JsonValue.Null, JsonValue.Create((object)null)); - Assert.Same(JsonValue.Null, JsonValue.Create((Instant?)null)); - } - - [Fact] - public void Should_cache_true() - { - Assert.Same(JsonValue.True, JsonValue.Create(true)); - } - - [Fact] - public void Should_cache_false() - { - Assert.Same(JsonValue.False, JsonValue.Create(false)); - } - - [Fact] - public void Should_cache_empty() - { - Assert.Same(JsonValue.Empty, JsonValue.Create(string.Empty)); - } - - [Fact] - public void Should_cache_zero() - { - Assert.Same(JsonValue.Zero, JsonValue.Create(0)); - } - - [Fact] - public void Should_boolean_from_object() - { - Assert.Equal(JsonValue.True, JsonValue.Create((object)true)); - } - - [Fact] - public void Should_create_value_from_instant() - { - var instant = Instant.FromUnixTimeSeconds(4123125455); - - Assert.Equal(instant.ToString(), JsonValue.Create(instant).ToString()); - } - - [Fact] - public void Should_create_value_from_instant_object() - { - var instant = Instant.FromUnixTimeSeconds(4123125455); - - Assert.Equal(instant.ToString(), JsonValue.Create((object)instant).ToString()); - } - - [Fact] - public void Should_create_array() - { - var json = JsonValue.Array(1, "2"); - - Assert.Equal("[1, \"2\"]", json.ToJsonString()); - Assert.Equal("[1, \"2\"]", json.ToString()); - } - - [Fact] - public void Should_create_object() - { - var json = JsonValue.Object().Add("key1", 1).Add("key2", "2"); - - Assert.Equal("{\"key1\":1, \"key2\":\"2\"}", json.ToJsonString()); - Assert.Equal("{\"key1\":1, \"key2\":\"2\"}", json.ToString()); - } - - [Fact] - public void Should_create_number() - { - var json = JsonValue.Create(123); - - Assert.Equal("123", json.ToJsonString()); - Assert.Equal("123", json.ToString()); - } - - [Fact] - public void Should_create_boolean_true() - { - var json = JsonValue.Create(true); - - Assert.Equal("true", json.ToJsonString()); - Assert.Equal("true", json.ToString()); - } - - [Fact] - public void Should_create_boolean_false() - { - var json = JsonValue.Create(false); - - Assert.Equal("false", json.ToJsonString()); - Assert.Equal("false", json.ToString()); - } - - [Fact] - public void Should_create_string() - { - var json = JsonValue.Create("hi"); - - Assert.Equal("\"hi\"", json.ToJsonString()); - Assert.Equal("hi", json.ToString()); - } - - [Fact] - public void Should_create_null() - { - var json = JsonValue.Create((object)null); - - Assert.Equal("null", json.ToJsonString()); - Assert.Equal("null", json.ToString()); - } - - [Fact] - public void Should_create_arrays_in_different_ways() - { - var numbers = new[] - { - JsonValue.Array(1.0f, 2.0f), - JsonValue.Array(JsonValue.Create(1.0f), JsonValue.Create(2.0f)) - }; - - Assert.Single(numbers.Distinct()); - Assert.Single(numbers.Select(x => x.GetHashCode()).Distinct()); - } - - [Fact] - public void Should_create_number_from_types() - { - var numbers = new[] - { - JsonValue.Create(12.0f), - JsonValue.Create(12.0), - JsonValue.Create(12L), - JsonValue.Create(12), - JsonValue.Create((object)12.0d), - JsonValue.Create((double?)12.0d) - }; - - Assert.Single(numbers.Distinct()); - Assert.Single(numbers.Select(x => x.GetHashCode()).Distinct()); - } - - [Fact] - public void Should_create_null_when_adding_null_to_array() - { - var array = JsonValue.Array(); - - array.Add(null); - - Assert.Same(JsonValue.Null, array[0]); - } - - [Fact] - public void Should_create_null_when_replacing_to_null_in_array() - { - var array = JsonValue.Array(1); - - array[0] = null; - - Assert.Same(JsonValue.Null, array[0]); - } - - [Fact] - public void Should_create_null_when_adding_null_to_object() - { - var obj = JsonValue.Object(); - - obj.Add("key", null); - - Assert.Same(JsonValue.Null, obj["key"]); - } - - [Fact] - public void Should_create_null_when_replacing_to_null_object() - { - var obj = JsonValue.Object(); - - obj["key"] = null; - - Assert.Same(JsonValue.Null, obj["key"]); - } - - [Fact] - public void Should_remove_value_from_object() - { - var obj = JsonValue.Object().Add("key", 1); - - obj.Remove("key"); - - Assert.False(obj.TryGetValue("key", out _)); - Assert.False(obj.ContainsKey("key")); - } - - [Fact] - public void Should_clear_values_from_object() - { - var obj = JsonValue.Object().Add("key", 1); - - obj.Clear(); - - Assert.False(obj.TryGetValue("key", out _)); - Assert.False(obj.ContainsKey("key")); - } - - [Fact] - public void Should_provide_collection_values_from_object() - { - var obj = JsonValue.Object().Add("11", "44").Add("22", "88"); - - var kvps = new[] - { - new KeyValuePair("11", JsonValue.Create("44")), - new KeyValuePair("22", JsonValue.Create("88")) - }; - - Assert.Equal(2, obj.Count); - - Assert.Equal(new[] { "11", "22" }, obj.Keys); - Assert.Equal(new[] { "44", "88" }, obj.Values.Select(x => x.ToString())); - - Assert.Equal(kvps, obj.ToArray()); - Assert.Equal(kvps, ((IEnumerable)obj).OfType>().ToArray()); - } - - [Fact] - public void Should_throw_exception_when_creation_value_from_invalid_type() - { - Assert.Throws(() => JsonValue.Create(Guid.Empty)); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs b/tests/Squidex.Infrastructure.Tests/LanguageTests.cs deleted file mode 100644 index c02983749..000000000 --- a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs +++ /dev/null @@ -1,141 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure -{ - public class LanguageTests - { - [Theory] - [InlineData("")] - [InlineData(" ")] - public void Should_throw_exception_if_getting_by_empty_key(string key) - { - Assert.Throws(() => Language.GetLanguage(key)); - } - - [Fact] - public void Should_throw_exception_if_getting_by_null_key() - { - Assert.Throws(() => Language.GetLanguage(null)); - } - - [Fact] - public void Should_throw_exception_if_getting_by_unsupported_language() - { - Assert.Throws(() => Language.GetLanguage("xy")); - } - - [Fact] - public void Should_provide_all_languages() - { - Assert.True(Language.AllLanguages.Count > 100); - } - - [Fact] - public void Should_return_true_for_valid_language() - { - Assert.True(Language.IsValidLanguage("de")); - } - - [Fact] - public void Should_return_false_for_invalid_language() - { - Assert.False(Language.IsValidLanguage("xx")); - } - - [Fact] - public void Should_make_implicit_conversion_to_language() - { - Language language = "de"; - - Assert.Equal(Language.DE, language); - } - - [Fact] - public void Should_make_implicit_conversion_to_string() - { - string iso2Code = Language.DE; - - Assert.Equal("de", iso2Code); - } - - [Theory] - [InlineData("de", "German")] - [InlineData("en", "English")] - [InlineData("sv", "Swedish")] - [InlineData("zh", "Chinese")] - public void Should_provide_correct_english_name(string key, string englishName) - { - var language = Language.GetLanguage(key); - - Assert.Equal(key, language.Iso2Code); - Assert.Equal(englishName, language.EnglishName); - Assert.Equal(englishName, language.ToString()); - } - - [Theory] - [InlineData("en", "en")] - [InlineData("en ", "en")] - [InlineData("EN", "en")] - [InlineData("EN ", "en")] - public void Should_parse_valid_languages(string input, string languageCode) - { - var language = Language.ParseOrNull(input); - - Assert.Equal(language, Language.GetLanguage(languageCode)); - } - - [Theory] - [InlineData("en-US", "en")] - [InlineData("en-GB", "en")] - [InlineData("EN-US", "en")] - [InlineData("EN-GB", "en")] - public void Should_parse_lanuages_from_culture(string input, string languageCode) - { - var language = Language.ParseOrNull(input); - - Assert.Equal(language, Language.GetLanguage(languageCode)); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData("xx")] - [InlineData("invalid")] - [InlineData(null)] - public void Should_parse_invalid_languages(string input) - { - var language = Language.ParseOrNull(input); - - Assert.Null(language); - } - - [Fact] - public void Should_serialize_and_deserialize_null_language() - { - Language value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_valid_language() - { - var value = Language.DE; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs b/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs deleted file mode 100644 index 62cd813c5..000000000 --- a/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Squidex.Infrastructure -{ - public sealed class LanguagesInitializerTests - { - [Fact] - public async Task Should_add_custom_languages() - { - var options = Options.Create(new LanguagesOptions - { - ["en-NO"] = "English (Norwegian)" - }); - - var sut = new LanguagesInitializer(options); - - await sut.InitializeAsync(); - - Assert.Equal("English (Norwegian)", Language.GetLanguage("en-NO").EnglishName); - } - - [Fact] - public async Task Should_not_add_invalid_languages() - { - var options = Options.Create(new LanguagesOptions - { - ["en-Error"] = null - }); - - var sut = new LanguagesInitializer(options); - - await sut.InitializeAsync(); - - Assert.False(Language.TryGetLanguage("en-Error", out _)); - } - - [Fact] - public async Task Should_not_override_existing_languages() - { - var options = Options.Create(new LanguagesOptions - { - ["de"] = "German (Germany)" - }); - - var sut = new LanguagesInitializer(options); - - await sut.InitializeAsync(); - - Assert.Equal("German", Language.GetLanguage("de").EnglishName); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs deleted file mode 100644 index f33a19c3e..000000000 --- a/tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Infrastructure.Log -{ - public class LockingLogStoreTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly ILockGrain lockGrain = A.Fake(); - private readonly ILogStore inner = A.Fake(); - private readonly LockingLogStore sut; - - public LockingLogStoreTests() - { - A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) - .Returns(lockGrain); - - sut = new LockingLogStore(inner, grainFactory); - } - - [Fact] - public async Task Should_lock_and_call_inner() - { - var stream = new MemoryStream(); - - var dateFrom = DateTime.Today; - var dateTo = dateFrom.AddDays(2); - - var key = "MyKey"; - - var releaseToken = Guid.NewGuid().ToString(); - - A.CallTo(() => lockGrain.AcquireLockAsync(key)) - .Returns(releaseToken); - - await sut.ReadLogAsync(key, dateFrom, dateTo, stream); - - A.CallTo(() => lockGrain.AcquireLockAsync(key)) - .MustHaveHappened(); - - A.CallTo(() => lockGrain.ReleaseLockAsync(releaseToken)) - .MustHaveHappened(); - - A.CallTo(() => inner.ReadLogAsync(key, dateFrom, dateTo, stream)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_write_default_message_if_lock_could_not_be_acquired() - { - var stream = new MemoryStream(); - - var dateFrom = DateTime.Today; - var dateTo = dateFrom.AddDays(2); - - var key = "MyKey"; - - A.CallTo(() => lockGrain.AcquireLockAsync(key)) - .Returns(Task.FromResult(null)); - - await sut.ReadLogAsync(key, dateFrom, dateTo, stream, TimeSpan.FromSeconds(1)); - - A.CallTo(() => lockGrain.AcquireLockAsync(key)) - .MustHaveHappened(); - - A.CallTo(() => lockGrain.ReleaseLockAsync(A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => inner.ReadLogAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - - Assert.True(stream.Length > 0); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs b/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs deleted file mode 100644 index fd4b8566d..000000000 --- a/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs +++ /dev/null @@ -1,525 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using FakeItEasy; -using Microsoft.Extensions.Logging; -using NodaTime; -using Squidex.Infrastructure.Log.Adapter; -using Xunit; - -namespace Squidex.Infrastructure.Log -{ - public class SemanticLogTests - { - private readonly List appenders = new List(); - private readonly List channels = new List(); - private readonly Lazy log; - private readonly ILogChannel channel = A.Fake(); - private string output = string.Empty; - - public SemanticLog Log - { - get { return log.Value; } - } - - public SemanticLogTests() - { - channels.Add(channel); - - A.CallTo(() => channel.Log(A.Ignored, A.Ignored)) - .Invokes((SemanticLogLevel level, string message) => - { - output += message; - }); - - log = new Lazy(() => new SemanticLog(channels, appenders, JsonLogWriterFactory.Default())); - } - - [Fact] - public void Should_log_multiple_lines() - { - Log.Log(SemanticLogLevel.Error, null, (_, w) => w.WriteProperty("logMessage", "Msg1")); - Log.Log(SemanticLogLevel.Error, null, (_, w) => w.WriteProperty("logMessage", "Msg2")); - - var expected1 = - LogTest(w => w - .WriteProperty("logLevel", "Error") - .WriteProperty("logMessage", "Msg1")); - - var expected2 = - LogTest(w => w - .WriteProperty("logLevel", "Error") - .WriteProperty("logMessage", "Msg2")); - - Assert.Equal(expected1 + expected2, output); - } - - [Fact] - public void Should_log_timestamp() - { - var clock = A.Fake(); - - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); - - appenders.Add(new TimestampLogAppender(clock)); - - Log.LogFatal(w => { /* Do Nothing */ }); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteProperty("timestamp", clock.GetCurrentInstant())); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_values_with_appender() - { - appenders.Add(new ConstantsLogWriter(w => w.WriteProperty("logValue", 1500))); - - Log.LogFatal(m => { }); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_application_info() - { - var sessionId = Guid.NewGuid(); - - appenders.Add(new ApplicationInfoLogAppender(GetType(), sessionId)); - - Log.LogFatal(m => { }); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteObject("app", a => a - .WriteProperty("name", "Squidex.Infrastructure.Tests") - .WriteProperty("version", "1.0.0.0") - .WriteProperty("sessionId", sessionId.ToString()))); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_trace() - { - Log.LogTrace(w => w.WriteProperty("logValue", 1500)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Trace") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_trace_and_context() - { - Log.LogTrace(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Trace") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_debug() - { - Log.LogDebug(w => w.WriteProperty("logValue", 1500)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Debug") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_debug_and_context() - { - Log.LogDebug(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Debug") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_information() - { - Log.LogInformation(w => w.WriteProperty("logValue", 1500)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Information") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_information_and_context() - { - Log.LogInformation(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Information") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_warning() - { - Log.LogWarning(w => w.WriteProperty("logValue", 1500)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Warning") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_warning_and_context() - { - Log.LogWarning(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Warning") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_warning_exception() - { - var exception = new InvalidOperationException(); - - Log.LogWarning(exception); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Warning") - .WriteException(exception)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_warning_exception_and_context() - { - var exception = new InvalidOperationException(); - - Log.LogWarning(exception, 1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Warning") - .WriteProperty("logValue", 1500) - .WriteException(exception)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_error() - { - Log.LogError(w => w.WriteProperty("logValue", 1500)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Error") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_error_and_context() - { - Log.LogError(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Error") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_error_exception() - { - var exception = new InvalidOperationException(); - - Log.LogError(exception); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Error") - .WriteException(exception)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_error_exception_and_context() - { - var exception = new InvalidOperationException(); - - Log.LogError(exception, 1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Error") - .WriteProperty("logValue", 1500) - .WriteException(exception)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_fatal() - { - Log.LogFatal(w => w.WriteProperty("logValue", 1500)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_fatal_and_context() - { - Log.LogFatal(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_fatal_exception() - { - var exception = new InvalidOperationException(); - - Log.LogFatal(exception); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteException(exception)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_fatal_exception_and_context() - { - var exception = new InvalidOperationException(); - - Log.LogFatal(exception, 1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteProperty("logValue", 1500) - .WriteException(exception)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_nothing_when_exception_is_null() - { - Log.LogFatal((Exception)null); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal")); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_measure_trace() - { - Log.MeasureTrace(w => w.WriteProperty("message", "My Message")).Dispose(); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Trace") - .WriteProperty("message", "My Message") - .WriteProperty("elapsedMs", 0)); - - Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); - } - - [Fact] - public void Should_measure_trace_with_contex() - { - Log.MeasureTrace("My Message", (ctx, w) => w.WriteProperty("message", ctx)).Dispose(); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Trace") - .WriteProperty("message", "My Message") - .WriteProperty("elapsedMs", 0)); - - Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); - } - - [Fact] - public void Should_measure_debug() - { - Log.MeasureDebug(w => w.WriteProperty("message", "My Message")).Dispose(); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Debug") - .WriteProperty("message", "My Message") - .WriteProperty("elapsedMs", 0)); - - Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); - } - - [Fact] - public void Should_measure_debug_with_contex() - { - Log.MeasureDebug("My Message", (ctx, w) => w.WriteProperty("message", ctx)).Dispose(); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Debug") - .WriteProperty("message", "My Message") - .WriteProperty("elapsedMs", 0)); - - Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); - } - - [Fact] - public void Should_measure_information() - { - Log.MeasureInformation(w => w.WriteProperty("message", "My Message")).Dispose(); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Information") - .WriteProperty("message", "My Message") - .WriteProperty("elapsedMs", 0)); - - Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); - } - - [Fact] - public void Should_measure_information_with_contex() - { - Log.MeasureInformation("My Message", (ctx, w) => w.WriteProperty("message", ctx)).Dispose(); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Information") - .WriteProperty("message", "My Message") - .WriteProperty("elapsedMs", 0)); - - Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); - } - - [Fact] - public void Should_log_with_extensions_logger() - { - var exception = new InvalidOperationException(); - - var loggerFactory = - new LoggerFactory() - .AddSemanticLog(Log); - var loggerInstance = loggerFactory.CreateLogger(); - - loggerInstance.LogCritical(new EventId(123, "EventName"), exception, "Log {0}", 123); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteProperty("message", "Log 123") - .WriteObject("eventId", e => e - .WriteProperty("id", 123) - .WriteProperty("name", "EventName")) - .WriteException(exception) - .WriteProperty("category", "Squidex.Infrastructure.Log.SemanticLogTests")); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_catch_all_exceptions_from_all_channels_when_exceptions_are_thrown() - { - var exception1 = new InvalidOperationException(); - var exception2 = new InvalidOperationException(); - - var channel1 = A.Fake(); - var channel2 = A.Fake(); - - A.CallTo(() => channel1.Log(A.Ignored, A.Ignored)).Throws(exception1); - A.CallTo(() => channel2.Log(A.Ignored, A.Ignored)).Throws(exception2); - - var sut = new SemanticLog(new[] { channel1, channel2 }, Enumerable.Empty(), JsonLogWriterFactory.Default()); - - try - { - sut.Log(SemanticLogLevel.Debug, null, (_, w) => w.WriteProperty("should", "throw")); - - Assert.False(true); - } - catch (AggregateException ex) - { - Assert.Equal(exception1, ex.InnerExceptions[0]); - Assert.Equal(exception2, ex.InnerExceptions[1]); - } - } - - private static string LogTest(Action writer) - { - var sut = JsonLogWriterFactory.Default().Create(); - - writer(sut); - - return sut.ToString(); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs b/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs deleted file mode 100644 index 6237b3b04..000000000 --- a/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs +++ /dev/null @@ -1,167 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; -using Xunit; - -namespace Squidex.Infrastructure.Migrations -{ - public class MigratorTests - { - private readonly IMigrationStatus status = A.Fake(); - private readonly IMigrationPath path = A.Fake(); - private readonly ISemanticLog log = A.Fake(); - private readonly List<(int From, int To, IMigration Migration)> migrations = new List<(int From, int To, IMigration Migration)>(); - - public sealed class InMemoryStatus : IMigrationStatus - { - private readonly object lockObject = new object(); - private int version; - private bool isLocked; - - public Task GetVersionAsync() - { - return Task.FromResult(version); - } - - public Task TryLockAsync() - { - var lockAcquired = false; - - lock (lockObject) - { - if (!isLocked) - { - isLocked = true; - - lockAcquired = true; - } - } - - return Task.FromResult(lockAcquired); - } - - public Task UnlockAsync(int newVersion) - { - lock (lockObject) - { - isLocked = false; - - version = newVersion; - } - - return TaskHelper.Done; - } - } - - public MigratorTests() - { - A.CallTo(() => path.GetNext(A.Ignored)) - .ReturnsLazily((int v) => - { - var m = migrations.Where(x => x.From == v).ToList(); - - return m.Count == 0 ? (0, null) : (migrations.Max(x => x.To), migrations.Select(x => x.Migration)); - }); - - A.CallTo(() => status.GetVersionAsync()).Returns(0); - A.CallTo(() => status.TryLockAsync()).Returns(true); - } - - [Fact] - public async Task Should_migrate_step_by_step() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_1_2 = BuildMigration(1, 2); - var migrator_2_3 = BuildMigration(2, 3); - - var sut = new Migrator(status, path, log); - - await sut.MigrateAsync(); - - A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); - A.CallTo(() => migrator_2_3.UpdateAsync()).MustHaveHappened(); - - A.CallTo(() => status.UnlockAsync(3)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_unlock_when_migration_failed() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_1_2 = BuildMigration(1, 2); - var migrator_2_3 = BuildMigration(2, 3); - - var sut = new Migrator(status, path, log); - - A.CallTo(() => migrator_1_2.UpdateAsync()).Throws(new ArgumentException()); - - await Assert.ThrowsAsync(() => sut.MigrateAsync()); - - A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); - A.CallTo(() => migrator_2_3.UpdateAsync()).MustNotHaveHappened(); - - A.CallTo(() => status.UnlockAsync(0)).MustHaveHappened(); - } - - [Fact] - public async Task Should_log_exception_when_migration_failed() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_1_2 = BuildMigration(1, 2); - - var ex = new InvalidOperationException(); - - A.CallTo(() => migrator_0_1.UpdateAsync()) - .Throws(ex); - - var sut = new Migrator(status, path, log); - - await Assert.ThrowsAsync(() => sut.MigrateAsync()); - - A.CallTo(() => log.Log(SemanticLogLevel.Fatal, default, A>.Ignored)) - .MustHaveHappened(); - - A.CallTo(() => migrator_1_2.UpdateAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_prevent_multiple_updates() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_1_2 = BuildMigration(1, 2); - - var sut = new Migrator(new InMemoryStatus(), path, log) { LockWaitMs = 2 }; - - await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(() => sut.MigrateAsync()))); - - A.CallTo(() => migrator_0_1.UpdateAsync()) - .MustHaveHappened(1, Times.Exactly); - A.CallTo(() => migrator_1_2.UpdateAsync()) - .MustHaveHappened(1, Times.Exactly); - } - - private IMigration BuildMigration(int fromVersion, int toVersion) - { - var migration = A.Fake(); - - migrations.Add((fromVersion, toVersion, migration)); - - return migration; - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs deleted file mode 100644 index 33b8da4cf..000000000 --- a/tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs +++ /dev/null @@ -1,169 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Infrastructure.Tasks; -using Xunit; - -namespace Squidex.Infrastructure.MongoDb -{ - public class MongoExtensionsTests - { - public sealed class Cursor : IAsyncCursor - { - private readonly List items = new List(); - private int index = -1; - - public IEnumerable Current - { - get - { - if (items[index] is Exception ex) - { - throw ex; - } - - return Enumerable.Repeat((T)items[index], 1); - } - } - - public Cursor Add(params T[] newItems) - { - foreach (var item in newItems) - { - items.Add(item); - } - - return this; - } - - public Cursor Add(Exception ex) - { - items.Add(ex); - - return this; - } - - public void Dispose() - { - } - - public bool MoveNext(CancellationToken cancellationToken = default) - { - index++; - - return index < items.Count; - } - - public async Task MoveNextAsync(CancellationToken cancellationToken = default) - { - await Task.Delay(1, cancellationToken); - - return MoveNext(cancellationToken); - } - } - - [Fact] - public async Task Should_enumerate_over_items() - { - var result = new List(); - - var cursor = new Cursor().Add(0, 1, 2, 3, 4, 5); - - await cursor.ForEachPipelineAsync(x => - { - result.Add(x); - return TaskHelper.Done; - }); - - Assert.Equal(new List { 0, 1, 2, 3, 4, 5 }, result); - } - - [Fact] - public async Task Should_break_when_cursor_failed() - { - var ex = new InvalidOperationException(); - - var result = new List(); - - using (var cursor = new Cursor().Add(0, 1, 2).Add(ex).Add(3, 4, 5)) - { - await Assert.ThrowsAsync(() => - { - return cursor.ForEachPipelineAsync(x => - { - result.Add(x); - return TaskHelper.Done; - }); - }); - } - - Assert.Equal(new List { 0, 1, 2 }, result); - } - - [Fact] - public async Task Should_break_when_handler_failed() - { - var ex = new InvalidOperationException(); - - var result = new List(); - - using (var cursor = new Cursor().Add(0, 1, 2, 3, 4, 5)) - { - await Assert.ThrowsAsync(() => - { - return cursor.ForEachPipelineAsync(x => - { - if (x == 2) - { - throw ex; - } - - result.Add(x); - return TaskHelper.Done; - }); - }); - } - - Assert.Equal(new List { 0, 1 }, result); - } - - [Fact] - public async Task Should_stop_when_cancelled1() - { - using (var cts = new CancellationTokenSource()) - { - var result = new List(); - - using (var cursor = new Cursor().Add(0, 1, 2, 3, 4, 5)) - { - await Assert.ThrowsAnyAsync(() => - { - return cursor.ForEachPipelineAsync(x => - { - if (x == 2) - { - cts.Cancel(); - } - - result.Add(x); - - return TaskHelper.Done; - }, cts.Token); - }); - } - - Assert.Equal(new List { 0, 1, 2 }, result); - } - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs b/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs deleted file mode 100644 index 44a09138a..000000000 --- a/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure -{ - public class NamedIdTests - { - [Fact] - public void Should_instantiate_token() - { - var id = Guid.NewGuid(); - - var namedId = NamedId.Of(id, "my-name"); - - Assert.Equal(id, namedId.Id); - Assert.Equal("my-name", namedId.Name); - } - - [Fact] - public void Should_convert_named_id_to_string() - { - var id = Guid.NewGuid(); - - var namedId = NamedId.Of(id, "my-name"); - - Assert.Equal($"{id},my-name", namedId.ToString()); - } - - [Fact] - public void Should_make_correct_equal_comparisons() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var named_id1_name1_a = NamedId.Of(id1, "name1"); - var named_id1_name1_b = NamedId.Of(id1, "name1"); - - var named_id2_name1 = NamedId.Of(id2, "name1"); - var named_id1_name2 = NamedId.Of(id1, "name2"); - - Assert.Equal(named_id1_name1_a, named_id1_name1_b); - Assert.Equal(named_id1_name1_a.GetHashCode(), named_id1_name1_b.GetHashCode()); - Assert.True(named_id1_name1_a.Equals((object)named_id1_name1_b)); - - Assert.NotEqual(named_id1_name1_a, named_id2_name1); - Assert.NotEqual(named_id1_name1_a.GetHashCode(), named_id2_name1.GetHashCode()); - Assert.False(named_id1_name1_a.Equals((object)named_id2_name1)); - - Assert.NotEqual(named_id1_name1_a, named_id1_name2); - Assert.NotEqual(named_id1_name1_a.GetHashCode(), named_id1_name2.GetHashCode()); - Assert.False(named_id1_name1_a.Equals((object)named_id1_name2)); - } - - [Fact] - public void Should_serialize_and_deserialize_null_guid_token() - { - NamedId value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_valid_guid_token() - { - var value = NamedId.Of(Guid.NewGuid(), "my-name"); - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_null_long_token() - { - NamedId value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_valid_long_token() - { - var value = NamedId.Of(123L, "my-name"); - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_null_string_token() - { - NamedId value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_valid_string_token() - { - var value = NamedId.Of(Guid.NewGuid().ToString(), "my-name"); - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_throw_exception_if_string_id_is_not_valid() - { - Assert.ThrowsAny(() => JsonHelper.Deserialize>("123")); - } - - [Fact] - public void Should_throw_exception_if_long_id_is_not_valid() - { - Assert.ThrowsAny(() => JsonHelper.Deserialize>("invalid-long,name")); - } - - [Fact] - public void Should_throw_exception_if_guid_id_is_not_valid() - { - Assert.ThrowsAny(() => JsonHelper.Deserialize>("invalid-guid,name")); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs deleted file mode 100644 index dc1e277a0..000000000 --- a/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs +++ /dev/null @@ -1,197 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Xunit; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public class UniqueNameIndexGrainTests - { - private readonly IGrainState> grainState = A.Fake>>(); - private readonly NamedId id1 = NamedId.Of(Guid.NewGuid(), "my-name1"); - private readonly NamedId id2 = NamedId.Of(Guid.NewGuid(), "my-name2"); - private readonly UniqueNameIndexGrain, Guid> sut; - - public UniqueNameIndexGrainTests() - { - A.CallTo(() => grainState.ClearAsync()) - .Invokes(() => grainState.Value = new UniqueNameIndexState()); - - sut = new UniqueNameIndexGrain, Guid>(grainState); - } - - [Fact] - public async Task Should_not_write_to_state_for_reservation() - { - await sut.ReserveAsync(id1.Id, id1.Name); - - A.CallTo(() => grainState.WriteAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_add_to_index_if_reservation_token_acquired() - { - await AddAsync(id1); - - var result = await sut.GetIdAsync(id1.Name); - - Assert.Equal(id1.Id, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_make_reservation_if_name_already_reserved() - { - await sut.ReserveAsync(id1.Id, id1.Name); - - var newToken = await sut.ReserveAsync(id1.Id, id1.Name); - - Assert.Null(newToken); - } - - [Fact] - public async Task Should_not_make_reservation_if_name_taken() - { - await AddAsync(id1); - - var newToken = await sut.ReserveAsync(id1.Id, id1.Name); - - Assert.Null(newToken); - } - - [Fact] - public async Task Should_provide_number_of_entries() - { - await AddAsync(id1); - await AddAsync(id2); - - var count = await sut.CountAsync(); - - Assert.Equal(2, count); - } - - [Fact] - public async Task Should_clear_all_entries() - { - await AddAsync(id1); - await AddAsync(id2); - - await sut.ClearAsync(); - - var count = await sut.CountAsync(); - - Assert.Equal(0, count); - } - - [Fact] - public async Task Should_make_reservation_after_reservation_removed() - { - var token = await sut.ReserveAsync(id1.Id, id1.Name); - - await sut.RemoveReservationAsync(token); - - var newToken = await sut.ReserveAsync(id1.Id, id1.Name); - - Assert.NotNull(newToken); - } - - [Fact] - public async Task Should_make_reservation_after_id_removed() - { - await AddAsync(id1); - - await sut.RemoveAsync(id1.Id); - - var newToken = await sut.ReserveAsync(id1.Id, id1.Name); - - Assert.NotNull(newToken); - } - - [Fact] - public async Task Should_remove_id_from_index() - { - await AddAsync(id1); - - await sut.RemoveAsync(id1.Id); - - var result = await sut.GetIdAsync(id1.Name); - - Assert.Equal(Guid.Empty, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappenedTwiceExactly(); - } - - [Fact] - public async Task Should_not_write_to_state_if_nothing_removed() - { - await sut.RemoveAsync(id1.Id); - - A.CallTo(() => grainState.WriteAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_ignore_error_if_removing_reservation_with_Invalid_token() - { - await sut.RemoveReservationAsync(null); - } - - [Fact] - public async Task Should_ignore_error_if_completing_reservation_with_Invalid_token() - { - await sut.AddAsync(null); - } - - [Fact] - public async Task Should_replace_ids_on_rebuild() - { - var state = new Dictionary - { - [id1.Name] = id1.Id, - [id2.Name] = id2.Id - }; - - await sut.RebuildAsync(state); - - Assert.Equal(id1.Id, await sut.GetIdAsync(id1.Name)); - Assert.Equal(id2.Id, await sut.GetIdAsync(id2.Name)); - - var result = await sut.GetIdsAsync(); - - Assert.Equal(new List { id1.Id, id2.Id }, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_provide_multiple_ids_by_names() - { - await AddAsync(id1); - await AddAsync(id2); - - var result = await sut.GetIdsAsync(new string[] { id1.Name, id2.Name, "not-found" }); - - Assert.Equal(new List { id1.Id, id2.Id }, result); - } - - private async Task AddAsync(NamedId id) - { - var token = await sut.ReserveAsync(id.Id, id.Name); - - await sut.AddAsync(token); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs deleted file mode 100644 index 11deb02cc..000000000 --- a/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ========================================================================== -// JsonExternalSerializerTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.IO; -using FakeItEasy; -using Orleans.Serialization; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Orleans -{ - public class JsonExternalSerializerTests - { - public JsonExternalSerializerTests() - { - J.DefaultSerializer = JsonHelper.DefaultSerializer; - } - - [Fact] - public void Should_not_copy_null() - { - var v = (string)null; - var c = J.Copy(v, null); - - Assert.Null(c); - } - - [Fact] - public void Should_copy_null_json() - { - var v = new J>(null); - var c = (J>)J.Copy(v, null); - - Assert.Null(c.Value); - } - - [Fact] - public void Should_not_copy_immutable_values() - { - var v = new List { 1, 2, 3 }.AsJ(); - var c = (J>)J.Copy(v, null); - - Assert.Same(v.Value, c.Value); - } - - [Fact] - public void Should_serialize_and_deserialize_value() - { - SerializeAndDeserialize(ArrayOfLength(100), Assert.Equal); - } - - [Fact] - public void Should_serialize_and_deserialize_large_value() - { - SerializeAndDeserialize(ArrayOfLength(8000), Assert.Equal); - } - - private static void SerializeAndDeserialize(T value, Action equals) where T : class - { - var buffer = new MemoryStream(); - - J.Serialize(J.Of(value), CreateWriter(buffer), typeof(T)); - - buffer.Position = 0; - - var copy = (J)J.Deserialize(typeof(J), CreateReader(buffer)); - - equals(copy.Value, value); - - Assert.NotSame(value, copy.Value); - } - - private static DeserializationContext CreateReader(MemoryStream buffer) - { - var reader = A.Fake(); - - A.CallTo(() => reader.ReadByteArray(A.Ignored, A.Ignored, A.Ignored)) - .Invokes(new Action((b, o, l) => buffer.Read(b, o, l))); - A.CallTo(() => reader.CurrentPosition) - .ReturnsLazily(x => (int)buffer.Position); - A.CallTo(() => reader.Length) - .ReturnsLazily(x => (int)buffer.Length); - - return new DeserializationContext(null) { StreamReader = reader }; - } - - private static SerializationContext CreateWriter(MemoryStream buffer) - { - var writer = A.Fake(); - - A.CallTo(() => writer.Write(A.Ignored, A.Ignored, A.Ignored)) - .Invokes(new Action(buffer.Write)); - A.CallTo(() => writer.CurrentOffset) - .ReturnsLazily(x => (int)buffer.Position); - - return new SerializationContext(null) { StreamWriter = writer }; - } - - private static List ArrayOfLength(int length) - { - var result = new List(); - - for (var i = 0; i < length; i++) - { - result.Add(i); - } - - return result; - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/LockGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/LockGrainTests.cs deleted file mode 100644 index ead17c4d1..000000000 --- a/tests/Squidex.Infrastructure.Tests/Orleans/LockGrainTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Xunit; - -namespace Squidex.Infrastructure.Orleans -{ - public class LockGrainTests - { - private readonly LockGrain sut = new LockGrain(); - - [Fact] - public async Task Should_not_acquire_lock_when_locked() - { - var releaseLock1 = await sut.AcquireLockAsync("Key1"); - var releaseLock2 = await sut.AcquireLockAsync("Key1"); - - Assert.NotNull(releaseLock1); - Assert.Null(releaseLock2); - } - - [Fact] - public async Task Should_acquire_lock_with_other_key() - { - var releaseLock1 = await sut.AcquireLockAsync("Key1"); - var releaseLock2 = await sut.AcquireLockAsync("Key2"); - - Assert.NotNull(releaseLock1); - Assert.NotNull(releaseLock2); - } - - [Fact] - public async Task Should_acquire_lock_after_released() - { - var releaseLock1 = await sut.AcquireLockAsync("Key1"); - - await sut.ReleaseLockAsync(releaseLock1); - - var releaseLock2 = await sut.AcquireLockAsync("Key1"); - - Assert.NotNull(releaseLock1); - Assert.NotNull(releaseLock2); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs b/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs deleted file mode 100644 index 9cde6d797..000000000 --- a/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs +++ /dev/null @@ -1,382 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using NJsonSchema; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.TestHelpers; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Infrastructure.Queries -{ - public sealed class JsonQueryConversionTests - { - private readonly List errors = new List(); - private readonly JsonSchema schema = new JsonSchema(); - - public JsonQueryConversionTests() - { - var nested = new JsonSchemaProperty { Title = "nested" }; - - nested.Properties["property"] = new JsonSchemaProperty - { - Type = JsonObjectType.String - }; - - schema.Properties["boolean"] = new JsonSchemaProperty - { - Type = JsonObjectType.Boolean - }; - - schema.Properties["datetime"] = new JsonSchemaProperty - { - Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime - }; - - schema.Properties["guid"] = new JsonSchemaProperty - { - Type = JsonObjectType.String, Format = JsonFormatStrings.Guid - }; - - schema.Properties["integer"] = new JsonSchemaProperty - { - Type = JsonObjectType.Integer - }; - - schema.Properties["number"] = new JsonSchemaProperty - { - Type = JsonObjectType.Number - }; - - schema.Properties["string"] = new JsonSchemaProperty - { - Type = JsonObjectType.String - }; - - schema.Properties["stringArray"] = new JsonSchemaProperty - { - Item = new JsonSchema - { - Type = JsonObjectType.String - }, - Type = JsonObjectType.Array - }; - - schema.Properties["object"] = nested; - - schema.Properties["reference"] = new JsonSchemaProperty - { - Reference = nested - }; - } - - [Fact] - public void Should_add_error_if_property_does_not_exist() - { - var json = new { path = "notfound", op = "eq", value = 1 }; - - AssertErrors(json, "Path 'notfound' does not point to a valid property in the model."); - } - - [Fact] - public void Should_add_error_if_nested_property_does_not_exist() - { - var json = new { path = "object.notfound", op = "eq", value = 1 }; - - AssertErrors(json, "'notfound' is not a property of 'nested'."); - } - - [Theory] - [InlineData("contains", "contains(datetime, 2012-11-10T09:08:07Z)")] - [InlineData("empty", "empty(datetime)")] - [InlineData("endswith", "endsWith(datetime, 2012-11-10T09:08:07Z)")] - [InlineData("eq", "datetime == 2012-11-10T09:08:07Z")] - [InlineData("ge", "datetime >= 2012-11-10T09:08:07Z")] - [InlineData("gt", "datetime > 2012-11-10T09:08:07Z")] - [InlineData("le", "datetime <= 2012-11-10T09:08:07Z")] - [InlineData("lt", "datetime < 2012-11-10T09:08:07Z")] - [InlineData("ne", "datetime != 2012-11-10T09:08:07Z")] - [InlineData("startswith", "startsWith(datetime, 2012-11-10T09:08:07Z)")] - public void Should_parse_datetime_string_filter(string op, string expected) - { - var json = new { path = "datetime", op, value = "2012-11-10T09:08:07Z" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_parse_date_string_filter() - { - var json = new { path = "datetime", op = "eq", value = "2012-11-10" }; - - AssertFilter(json, "datetime == 2012-11-10T00:00:00Z"); - } - - [Fact] - public void Should_add_error_if_datetime_string_property_got_invalid_string_value() - { - var json = new { path = "datetime", op = "eq", value = "invalid" }; - - AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got invalid String."); - } - - [Fact] - public void Should_add_error_if_datetime_string_property_got_invalid_value() - { - var json = new { path = "datetime", op = "eq", value = 1 }; - - AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got Number."); - } - - [Theory] - [InlineData("contains", "contains(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - [InlineData("empty", "empty(guid)")] - [InlineData("endswith", "endsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - [InlineData("eq", "guid == bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("ge", "guid >= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("gt", "guid > bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("le", "guid <= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("lt", "guid < bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("ne", "guid != bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("startswith", "startsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - public void Should_parse_guid_string_filter(string op, string expected) - { - var json = new { path = "guid", op, value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_guid_string_property_got_invalid_string_value() - { - var json = new { path = "guid", op = "eq", value = "invalid" }; - - AssertErrors(json, "Expected Guid String for path 'guid', but got invalid String."); - } - - [Fact] - public void Should_add_error_if_guid_string_property_got_invalid_value() - { - var json = new { path = "guid", op = "eq", value = 1 }; - - AssertErrors(json, "Expected Guid String for path 'guid', but got Number."); - } - - [Theory] - [InlineData("contains", "contains(string, 'Hello')")] - [InlineData("empty", "empty(string)")] - [InlineData("endswith", "endsWith(string, 'Hello')")] - [InlineData("eq", "string == 'Hello'")] - [InlineData("ge", "string >= 'Hello'")] - [InlineData("gt", "string > 'Hello'")] - [InlineData("le", "string <= 'Hello'")] - [InlineData("lt", "string < 'Hello'")] - [InlineData("ne", "string != 'Hello'")] - [InlineData("startswith", "startsWith(string, 'Hello')")] - public void Should_parse_string_filter(string op, string expected) - { - var json = new { path = "string", op, value = "Hello" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_string_property_got_invalid_value() - { - var json = new { path = "string", op = "eq", value = 1 }; - - AssertErrors(json, "Expected String for path 'string', but got Number."); - } - - [Fact] - public void Should_parse_string_in_filter() - { - var json = new { path = "string", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "string in ['Hello']"); - } - - [Fact] - public void Should_parse_nested_string_filter() - { - var json = new { path = "object.property", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "object.property in ['Hello']"); - } - - [Fact] - public void Should_parse_referenced_string_filter() - { - var json = new { path = "reference.property", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "reference.property in ['Hello']"); - } - - [Theory] - [InlineData("eq", "number == 12")] - [InlineData("ge", "number >= 12")] - [InlineData("gt", "number > 12")] - [InlineData("le", "number <= 12")] - [InlineData("lt", "number < 12")] - [InlineData("ne", "number != 12")] - public void Should_parse_number_filter(string op, string expected) - { - var json = new { path = "number", op, value = 12 }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_number_property_got_invalid_value() - { - var json = new { path = "number", op = "eq", value = true }; - - AssertErrors(json, "Expected Number for path 'number', but got Boolean."); - } - - [Fact] - public void Should_parse_number_in_filter() - { - var json = new { path = "number", op = "in", value = new[] { 12 } }; - - AssertFilter(json, "number in [12]"); - } - - [Theory] - [InlineData("eq", "boolean == True")] - [InlineData("ne", "boolean != True")] - public void Should_parse_boolean_filter(string op, string expected) - { - var json = new { path = "boolean", op, value = true }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_boolean_property_got_invalid_value() - { - var json = new { path = "boolean", op = "eq", value = 1 }; - - AssertErrors(json, "Expected Boolean for path 'boolean', but got Number."); - } - - [Fact] - public void Should_parse_boolean_in_filter() - { - var json = new { path = "boolean", op = "in", value = new[] { true } }; - - AssertFilter(json, "boolean in [True]"); - } - - [Theory] - [InlineData("empty", "empty(stringArray)")] - [InlineData("eq", "stringArray == 'Hello'")] - [InlineData("ne", "stringArray != 'Hello'")] - public void Should_parse_array_filter(string op, string expected) - { - var json = new { path = "stringArray", op, value = "Hello" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_parse_array_in_filter() - { - var json = new { path = "stringArray", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "stringArray in ['Hello']"); - } - - [Fact] - public void Should_add_error_when_using_array_value_for_non_allowed_operator() - { - var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; - - AssertErrors(json, "Array value is not allowed for 'Equals' operator and path 'string'."); - } - - [Fact] - public void Should_parse_query() - { - var json = new { skip = 10, take = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; - - AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); - } - - [Fact] - public void Should_parse_query_with_sorting() - { - var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; - - AssertQuery(json, "Sort: string Ascending"); - } - - [Fact] - public void Should_throw_exception_for_invalid_query() - { - var json = new { sort = new[] { new { path = "invalid", order = "ascending" } } }; - - Assert.Throws(() => AssertQuery(json, null)); - } - - [Fact] - public void Should_throw_exception_when_parsing_invalid_json() - { - var json = "invalid"; - - Assert.Throws(() => AssertQuery(json, null)); - } - - private void AssertQuery(object json, string expectedFilter) - { - var filter = ConvertQuery(json); - - Assert.Empty(errors); - - Assert.Equal(expectedFilter, filter); - } - - private void AssertFilter(object json, string expectedFilter) - { - var filter = ConvertFilter(json); - - Assert.Empty(errors); - - Assert.Equal(expectedFilter, filter); - } - - private void AssertErrors(object json, params string[] expectedErrors) - { - var filter = ConvertFilter(json); - - Assert.Equal(expectedErrors.ToList(), errors); - - Assert.Null(filter); - } - - private string ConvertFilter(T value) - { - var json = JsonHelper.DefaultSerializer.Serialize(value, true); - - var jsonFilter = JsonHelper.DefaultSerializer.Deserialize>(json); - - return JsonFilterVisitor.Parse(jsonFilter, schema, errors)?.ToString(); - } - - private string ConvertQuery(T value) - { - var json = JsonHelper.DefaultSerializer.Serialize(value, true); - - var jsonFilter = schema.Parse(json, JsonHelper.DefaultSerializer); - - return jsonFilter.ToString(); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs b/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs deleted file mode 100644 index 3c1b86e7f..000000000 --- a/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Xunit; - -namespace Squidex.Infrastructure.Queries -{ - public class PascalCasePathConverterTests - { - [Fact] - public void Should_convert_property() - { - var source = ClrFilter.Eq("property", 1); - var result = PascalCasePathConverter.Transform(source); - - Assert.Equal("Property == 1", result.ToString()); - } - - [Fact] - public void Should_convert_properties() - { - var source = ClrFilter.Eq("root.child", 1); - var result = PascalCasePathConverter.Transform(source); - - Assert.Equal("Root.Child == 1", result.ToString()); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs b/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs deleted file mode 100644 index 91566e114..000000000 --- a/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs +++ /dev/null @@ -1,374 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using NJsonSchema; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.TestHelpers; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Infrastructure.Queries -{ - public sealed class QueryJsonConversionTests - { - private readonly List errors = new List(); - private readonly JsonSchema schema = new JsonSchema(); - - public QueryJsonConversionTests() - { - var nested = new JsonSchemaProperty { Title = "nested" }; - - nested.Properties["property"] = new JsonSchemaProperty - { - Type = JsonObjectType.String - }; - - schema.Properties["boolean"] = new JsonSchemaProperty - { - Type = JsonObjectType.Boolean - }; - - schema.Properties["datetime"] = new JsonSchemaProperty - { - Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime - }; - - schema.Properties["guid"] = new JsonSchemaProperty - { - Type = JsonObjectType.String, Format = JsonFormatStrings.Guid - }; - - schema.Properties["integer"] = new JsonSchemaProperty - { - Type = JsonObjectType.Integer - }; - - schema.Properties["number"] = new JsonSchemaProperty - { - Type = JsonObjectType.Number - }; - - schema.Properties["string"] = new JsonSchemaProperty - { - Type = JsonObjectType.String - }; - - schema.Properties["stringArray"] = new JsonSchemaProperty - { - Item = new JsonSchema - { - Type = JsonObjectType.String - }, - Type = JsonObjectType.Array - }; - - schema.Properties["object"] = nested; - - schema.Properties["reference"] = new JsonSchemaProperty - { - Reference = nested - }; - } - - [Fact] - public void Should_add_error_if_property_does_not_exist() - { - var json = new { path = "notfound", op = "eq", value = 1 }; - - AssertErrors(json, "Path 'notfound' does not point to a valid property in the model."); - } - - [Fact] - public void Should_add_error_if_nested_property_does_not_exist() - { - var json = new { path = "object.notfound", op = "eq", value = 1 }; - - AssertErrors(json, "'notfound' is not a property of 'nested'."); - } - - [Theory] - [InlineData("contains", "contains(datetime, 2012-11-10T09:08:07Z)")] - [InlineData("empty", "empty(datetime)")] - [InlineData("endswith", "endsWith(datetime, 2012-11-10T09:08:07Z)")] - [InlineData("eq", "datetime == 2012-11-10T09:08:07Z")] - [InlineData("ge", "datetime >= 2012-11-10T09:08:07Z")] - [InlineData("gt", "datetime > 2012-11-10T09:08:07Z")] - [InlineData("le", "datetime <= 2012-11-10T09:08:07Z")] - [InlineData("lt", "datetime < 2012-11-10T09:08:07Z")] - [InlineData("ne", "datetime != 2012-11-10T09:08:07Z")] - [InlineData("startswith", "startsWith(datetime, 2012-11-10T09:08:07Z)")] - public void Should_parse_datetime_string_filter(string op, string expected) - { - var json = new { path = "datetime", op, value = "2012-11-10T09:08:07Z" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_datetime_string_property_got_invalid_string_value() - { - var json = new { path = "datetime", op = "eq", value = "invalid" }; - - AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got invalid String."); - } - - [Fact] - public void Should_add_error_if_datetime_string_property_got_invalid_value() - { - var json = new { path = "datetime", op = "eq", value = 1 }; - - AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got Number."); - } - - [Theory] - [InlineData("contains", "contains(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - [InlineData("empty", "empty(guid)")] - [InlineData("endswith", "endsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - [InlineData("eq", "guid == bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("ge", "guid >= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("gt", "guid > bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("le", "guid <= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("lt", "guid < bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("ne", "guid != bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("startswith", "startsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - public void Should_parse_guid_string_filter(string op, string expected) - { - var json = new { path = "guid", op, value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_guid_string_property_got_invalid_string_value() - { - var json = new { path = "guid", op = "eq", value = "invalid" }; - - AssertErrors(json, "Expected Guid String for path 'guid', but got invalid String."); - } - - [Fact] - public void Should_add_error_if_guid_string_property_got_invalid_value() - { - var json = new { path = "guid", op = "eq", value = 1 }; - - AssertErrors(json, "Expected Guid String for path 'guid', but got Number."); - } - - [Theory] - [InlineData("contains", "contains(string, 'Hello')")] - [InlineData("empty", "empty(string)")] - [InlineData("endswith", "endsWith(string, 'Hello')")] - [InlineData("eq", "string == 'Hello'")] - [InlineData("ge", "string >= 'Hello'")] - [InlineData("gt", "string > 'Hello'")] - [InlineData("le", "string <= 'Hello'")] - [InlineData("lt", "string < 'Hello'")] - [InlineData("ne", "string != 'Hello'")] - [InlineData("startswith", "startsWith(string, 'Hello')")] - public void Should_parse_string_filter(string op, string expected) - { - var json = new { path = "string", op, value = "Hello" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_string_property_got_invalid_value() - { - var json = new { path = "string", op = "eq", value = 1 }; - - AssertErrors(json, "Expected String for path 'string', but got Number."); - } - - [Fact] - public void Should_parse_string_in_filter() - { - var json = new { path = "string", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "string in ['Hello']"); - } - - [Fact] - public void Should_parse_nested_string_filter() - { - var json = new { path = "object.property", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "object.property in ['Hello']"); - } - - [Fact] - public void Should_parse_referenced_string_filter() - { - var json = new { path = "reference.property", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "reference.property in ['Hello']"); - } - - [Theory] - [InlineData("eq", "number == 12")] - [InlineData("ge", "number >= 12")] - [InlineData("gt", "number > 12")] - [InlineData("le", "number <= 12")] - [InlineData("lt", "number < 12")] - [InlineData("ne", "number != 12")] - public void Should_parse_number_filter(string op, string expected) - { - var json = new { path = "number", op, value = 12 }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_number_property_got_invalid_value() - { - var json = new { path = "number", op = "eq", value = true }; - - AssertErrors(json, "Expected Number for path 'number', but got Boolean."); - } - - [Fact] - public void Should_parse_number_in_filter() - { - var json = new { path = "number", op = "in", value = new[] { 12 } }; - - AssertFilter(json, "number in [12]"); - } - - [Theory] - [InlineData("eq", "boolean == True")] - [InlineData("ne", "boolean != True")] - public void Should_parse_boolean_filter(string op, string expected) - { - var json = new { path = "boolean", op, value = true }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_boolean_property_got_invalid_value() - { - var json = new { path = "boolean", op = "eq", value = 1 }; - - AssertErrors(json, "Expected Boolean for path 'boolean', but got Number."); - } - - [Fact] - public void Should_parse_boolean_in_filter() - { - var json = new { path = "boolean", op = "in", value = new[] { true } }; - - AssertFilter(json, "boolean in [True]"); - } - - [Theory] - [InlineData("empty", "empty(stringArray)")] - [InlineData("eq", "stringArray == 'Hello'")] - [InlineData("ne", "stringArray != 'Hello'")] - public void Should_parse_array_filter(string op, string expected) - { - var json = new { path = "stringArray", op, value = "Hello" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_parse_array_in_filter() - { - var json = new { path = "stringArray", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "stringArray in ['Hello']"); - } - - [Fact] - public void Should_add_error_when_using_array_value_for_non_allowed_operator() - { - var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; - - AssertErrors(json, "Array value is not allowed for 'Equals' operator and path 'string'."); - } - - [Fact] - public void Should_parse_query() - { - var json = new { skip = 10, take = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; - - AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); - } - - [Fact] - public void Should_parse_query_with_sorting() - { - var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; - - AssertQuery(json, "Sort: string Ascending"); - } - - [Fact] - public void Should_throw_exception_for_invalid_query() - { - var json = new { sort = new[] { new { path = "invalid", order = "ascending" } } }; - - Assert.Throws(() => AssertQuery(json, null)); - } - - [Fact] - public void Should_throw_exception_when_parsing_invalid_json() - { - var json = "invalid"; - - Assert.Throws(() => AssertQuery(json, null)); - } - - private void AssertQuery(object json, string expectedFilter) - { - var filter = ConvertQuery(json); - - Assert.Empty(errors); - - Assert.Equal(expectedFilter, filter); - } - - private void AssertFilter(object json, string expectedFilter) - { - var filter = ConvertFilter(json); - - Assert.Empty(errors); - - Assert.Equal(expectedFilter, filter); - } - - private void AssertErrors(object json, params string[] expectedErrors) - { - var filter = ConvertFilter(json); - - Assert.Equal(expectedErrors.ToList(), errors); - - Assert.Null(filter); - } - - private string ConvertFilter(T value) - { - var json = JsonHelper.DefaultSerializer.Serialize(value, true); - - var jsonFilter = JsonHelper.DefaultSerializer.Deserialize>(json); - - return JsonFilterVisitor.Parse(jsonFilter, schema, errors)?.ToString(); - } - - private string ConvertQuery(T value) - { - var json = JsonHelper.DefaultSerializer.Serialize(value, true); - - var jsonFilter = schema.Parse(json, JsonHelper.DefaultSerializer); - - return jsonFilter.ToString(); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs b/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs deleted file mode 100644 index bdac075d4..000000000 --- a/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs +++ /dev/null @@ -1,424 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.OData.Edm; -using Squidex.Infrastructure.Queries.OData; -using Xunit; - -namespace Squidex.Infrastructure.Queries -{ - public class QueryODataConversionTests - { - private static readonly IEdmModel EdmModel; - - static QueryODataConversionTests() - { - var entityType = new EdmEntityType("Squidex", "Users"); - - entityType.AddStructuralProperty("id", EdmPrimitiveTypeKind.Guid); - entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset); - entityType.AddStructuralProperty("isComicFigure", EdmPrimitiveTypeKind.Boolean); - entityType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty("birthday", EdmPrimitiveTypeKind.Date); - entityType.AddStructuralProperty("incomeCents", EdmPrimitiveTypeKind.Int64); - entityType.AddStructuralProperty("incomeMio", EdmPrimitiveTypeKind.Double); - entityType.AddStructuralProperty("age", EdmPrimitiveTypeKind.Int32); - - var container = new EdmEntityContainer("Squidex", "Container"); - - container.AddEntitySet("UserSet", entityType); - - var model = new EdmModel(); - - model.AddElement(container); - model.AddElement(entityType); - - EdmModel = model; - } - - [Fact] - public void Should_parse_query() - { - var parser = EdmModel.ParseQuery("$filter=firstName eq 'Dagobert'"); - - Assert.NotNull(parser); - } - - [Fact] - public void Should_parse_filter_when_type_is_datetime() - { - var i = Q("$filter=created eq 1988-01-19T12:00:00Z"); - var o = C("Filter: created == 1988-01-19T12:00:00Z"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_datetime_list() - { - var i = Q("$filter=created in ('1988-01-19T12:00:00Z')"); - var o = C("Filter: created in [1988-01-19T12:00:00Z]"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_date() - { - var i = Q("$filter=created eq 1988-01-19"); - var o = C("Filter: created == 1988-01-19T00:00:00Z"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_date_list() - { - var i = Q("$filter=created in ('1988-01-19')"); - var o = C("Filter: created in [1988-01-19T00:00:00Z]"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_guid() - { - var i = Q("$filter=id eq B5FE25E3-B262-4B17-91EF-B3772A6B62BB"); - var o = C("Filter: id == b5fe25e3-b262-4b17-91ef-b3772a6b62bb"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_guid_list() - { - var i = Q("$filter=id in ('B5FE25E3-B262-4B17-91EF-B3772A6B62BB')"); - var o = C("Filter: id in [b5fe25e3-b262-4b17-91ef-b3772a6b62bb]"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_null() - { - var i = Q("$filter=firstName eq null"); - var o = C("Filter: firstName == null"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_string() - { - var i = Q("$filter=firstName eq 'Dagobert'"); - var o = C("Filter: firstName == 'Dagobert'"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_string_list() - { - var i = Q("$filter=firstName in ('Dagobert')"); - var o = C("Filter: firstName in ['Dagobert']"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_boolean() - { - var i = Q("$filter=isComicFigure eq true"); - var o = C("Filter: isComicFigure == True"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_boolean_list() - { - var i = Q("$filter=isComicFigure in (true)"); - var o = C("Filter: isComicFigure in [True]"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_int32() - { - var i = Q("$filter=age eq 60"); - var o = C("Filter: age == 60"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_int32_list() - { - var i = Q("$filter=age in (60)"); - var o = C("Filter: age in [60]"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_int64() - { - var i = Q("$filter=incomeCents eq 31543143513456789"); - var o = C("Filter: incomeCents == 31543143513456789"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_int64_list() - { - var i = Q("$filter=incomeCents in (31543143513456789)"); - var o = C("Filter: incomeCents in [31543143513456789]"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_double() - { - var i = Q("$filter=incomeMio eq 5634474356.1233"); - var o = C("Filter: incomeMio == 5634474356.1233"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_double_list() - { - var i = Q("$filter=incomeMio in (5634474356.1233)"); - var o = C("Filter: incomeMio in [5634474356.1233]"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_negation() - { - var i = Q("$filter=not endswith(lastName, 'Duck')"); - var o = C("Filter: !(endsWith(lastName, 'Duck'))"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_startswith() - { - var i = Q("$filter=startswith(lastName, 'Duck')"); - var o = C("Filter: startsWith(lastName, 'Duck')"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_endswith() - { - var i = Q("$filter=endswith(lastName, 'Duck')"); - var o = C("Filter: endsWith(lastName, 'Duck')"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_empty() - { - var i = Q("$filter=empty(lastName)"); - var o = C("Filter: empty(lastName)"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_empty_to_true() - { - var i = Q("$filter=empty(lastName) eq true"); - var o = C("Filter: empty(lastName)"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_contains() - { - var i = Q("$filter=contains(lastName, 'Duck')"); - var o = C("Filter: contains(lastName, 'Duck')"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_contains_to_true() - { - var i = Q("$filter=contains(lastName, 'Duck') eq true"); - var o = C("Filter: contains(lastName, 'Duck')"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_contains_to_false() - { - var i = Q("$filter=contains(lastName, 'Duck') eq false"); - var o = C("Filter: !(contains(lastName, 'Duck'))"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_equals() - { - var i = Q("$filter=age eq 1"); - var o = C("Filter: age == 1"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_notequals() - { - var i = Q("$filter=age ne 1"); - var o = C("Filter: age != 1"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_lessthan() - { - var i = Q("$filter=age lt 1"); - var o = C("Filter: age < 1"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_lessthanorequal() - { - var i = Q("$filter=age le 1"); - var o = C("Filter: age <= 1"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_greaterthan() - { - var i = Q("$filter=age gt 1"); - var o = C("Filter: age > 1"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_greaterthanorequal() - { - var i = Q("$filter=age ge 1"); - var o = C("Filter: age >= 1"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_conjunction_and_contains() - { - var i = Q("$filter=contains(firstName, 'Sebastian') eq false and isComicFigure eq true"); - var o = C("Filter: (!(contains(firstName, 'Sebastian')) && isComicFigure == True)"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_conjunction() - { - var i = Q("$filter=age eq 1 and age eq 2"); - var o = C("Filter: (age == 1 && age == 2)"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_disjunction() - { - var i = Q("$filter=age eq 1 or age eq 2"); - var o = C("Filter: (age == 1 || age == 2)"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_full_text_numbers() - { - var i = Q("$search=\"33k\""); - var o = C("FullText: '33k'"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_full_text() - { - var i = Q("$search=Duck"); - var o = C("FullText: 'Duck'"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_full_text_and_multiple_terms() - { - var i = Q("$search=Dagobert or Donald"); - var o = C("FullText: 'Dagobert or Donald'"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_orderby_with_single_field() - { - var i = Q("$orderby=age desc"); - var o = C("Sort: age Descending"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_orderby_with_multiple_field() - { - var i = Q("$orderby=age, incomeMio desc"); - var o = C("Sort: age Ascending, incomeMio Descending"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_and_take() - { - var i = Q("$top=3&$skip=4"); - var o = C("Skip: 4; Take: 3"); - - Assert.Equal(o, i); - } - - private static string C(string value) - { - return value; - } - - private static string Q(string value) - { - var parser = EdmModel.ParseQuery(value); - - return parser.ToQuery().ToString(); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs b/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs deleted file mode 100644 index 0ebd75b3a..000000000 --- a/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs +++ /dev/null @@ -1,94 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Xunit; - -namespace Squidex.Infrastructure.Queries -{ - public class QueryOptimizationTests - { - [Fact] - public void Should_not_convert_optimize_valid_logical_filter() - { - var source = ClrFilter.Or(ClrFilter.Eq("path", 2), ClrFilter.Eq("path", 3)); - - var result = Optimizer.Optimize(source); - - Assert.Equal("(path == 2 || path == 3)", result.ToString()); - } - - [Fact] - public void Should_return_filter_When_logical_filter_has_one_child() - { - var source = ClrFilter.And(ClrFilter.Eq("path", 1), ClrFilter.Or()); - - var result = Optimizer.Optimize(source); - - Assert.Equal("path == 1", result.ToString()); - } - - [Fact] - public void Should_return_null_when_filters_of_logical_filter_get_optimized_away() - { - var source = ClrFilter.And(ClrFilter.And()); - - var result = Optimizer.Optimize(source); - - Assert.Null(result); - } - - [Fact] - public void Should_return_null_when_logical_filter_has_no_filter() - { - var source = ClrFilter.And(); - - var result = Optimizer.Optimize(source); - - Assert.Null(result); - } - - [Fact] - public void Should_return_null_when_filter_of_negation_get_optimized_away() - { - var source = ClrFilter.Not(ClrFilter.And()); - - var result = Optimizer.Optimize(source); - - Assert.Null(result); - } - - [Fact] - public void Should_invert_equals_not_filter() - { - var source = ClrFilter.Not(ClrFilter.Eq("path", 1)); - - var result = Optimizer.Optimize(source); - - Assert.Equal("path != 1", result.ToString()); - } - - [Fact] - public void Should_invert_notequals_not_filter() - { - var source = ClrFilter.Not(ClrFilter.Ne("path", 1)); - - var result = Optimizer.Optimize(source); - - Assert.Equal("path == 1", result.ToString()); - } - - [Fact] - public void Should_not_convert_number_operator() - { - var source = ClrFilter.Not(ClrFilter.Lt("path", 1)); - - var result = Optimizer.Optimize(source); - - Assert.Equal("!(path < 1)", result.ToString()); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs b/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs deleted file mode 100644 index 355aba360..000000000 --- a/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs +++ /dev/null @@ -1,122 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure -{ - public class RefTokenTests - { - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(":")] - [InlineData("user")] - public void Should_throw_exception_if_parsing_invalid_input(string input) - { - Assert.Throws(() => RefToken.Parse(input)); - } - - [Fact] - public void Should_instantiate_token() - { - var token = new RefToken("client", "client1"); - - Assert.Equal("client", token.Type); - Assert.Equal("client1", token.Identifier); - - Assert.True(token.IsClient); - } - - [Fact] - public void Should_instantiate_subject_token() - { - var token = new RefToken("subject", "client1"); - - Assert.True(token.IsSubject); - } - - [Fact] - public void Should_instantiate_token_and_lower_type() - { - var token = new RefToken("Client", "client1"); - - Assert.Equal("client", token.Type); - Assert.Equal("client1", token.Identifier); - } - - [Fact] - public void Should_parse_user_token_from_string() - { - var token = RefToken.Parse("client:client1"); - - Assert.Equal("client", token.Type); - Assert.Equal("client1", token.Identifier); - } - - [Fact] - public void Should_parse_user_token_with_colon_in_identifier() - { - var token = RefToken.Parse("client:client1:app"); - - Assert.Equal("client", token.Type); - Assert.Equal("client1:app", token.Identifier); - } - - [Fact] - public void Should_convert_user_token_to_string() - { - var token = RefToken.Parse("client:client1"); - - Assert.Equal("client:client1", token.ToString()); - } - - [Fact] - public void Should_make_correct_equal_comparisons() - { - var token_type1_id1_a = RefToken.Parse("type1:client1"); - var token_type1_id1_b = RefToken.Parse("type1:client1"); - - var token_type2_id1 = RefToken.Parse("type2:client1"); - var token_type1_id2 = RefToken.Parse("type1:client2"); - - Assert.Equal(token_type1_id1_a, token_type1_id1_b); - Assert.Equal(token_type1_id1_a.GetHashCode(), token_type1_id1_b.GetHashCode()); - Assert.True(token_type1_id1_a.Equals((object)token_type1_id1_b)); - - Assert.NotEqual(token_type1_id1_a, token_type2_id1); - Assert.NotEqual(token_type1_id1_a.GetHashCode(), token_type2_id1.GetHashCode()); - Assert.False(token_type1_id1_a.Equals((object)token_type2_id1)); - - Assert.NotEqual(token_type1_id1_a, token_type1_id2); - Assert.NotEqual(token_type1_id1_a.GetHashCode(), token_type1_id2.GetHashCode()); - Assert.False(token_type1_id1_a.Equals((object)token_type1_id2)); - } - - [Fact] - public void Should_serialize_and_deserialize_null_token() - { - RefToken value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_valid_token() - { - var value = RefToken.Parse("client:client1"); - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs b/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs deleted file mode 100644 index 36b770438..000000000 --- a/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs +++ /dev/null @@ -1,177 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Diagnostics; -using Xunit; - -namespace Squidex.Infrastructure.Reflection -{ - public class SimpleMapperTests - { - public class Class1Base - { - public T1 P1 { get; set; } - } - - public class Class1 : Class1Base - { - public T2 P2 { get; set; } - } - - public class Class2Base - { - public T2 P2 { get; set; } - } - - public class Class2 : Class2Base - { - public T3 P3 { get; set; } - } - - public class Readonly - { - public T P1 { get; } - } - - public class Writeonly - { - public T P1 - { - set { Debug.WriteLine(value); } - } - } - - [Fact] - public void Should_throw_exception_if_mapping_with_null_source() - { - Assert.Throws(() => SimpleMapper.Map((Class2)null, new Class2())); - } - - [Fact] - public void Should_throw_exception_if_mapping_with_null_target() - { - Assert.Throws(() => SimpleMapper.Map(new Class2(), (Class2)null)); - } - - [Fact] - public void Should_map_to_same_type() - { - var obj1 = new Class1 - { - P1 = 6, - P2 = 8 - }; - var obj2 = SimpleMapper.Map(obj1, new Class2()); - - Assert.Equal(8, obj2.P2); - Assert.Equal(0, obj2.P3); - } - - [Fact] - public void Should_map_all_properties() - { - var obj1 = new Class1 - { - P1 = 6, - P2 = 8 - }; - var obj2 = SimpleMapper.Map(obj1, new Class1()); - - Assert.Equal(6, obj2.P1); - Assert.Equal(8, obj2.P2); - } - - [Fact] - public void Should_map_to_convertible_type() - { - var obj1 = new Class1 - { - P1 = 6, - P2 = 8 - }; - var obj2 = SimpleMapper.Map(obj1, new Class2()); - - Assert.Equal(8, obj2.P2); - Assert.Equal(0, obj2.P3); - } - - [Fact] - public void Should_map_nullables() - { - var obj1 = new Class1 - { - P1 = true, - P2 = true - }; - var obj2 = SimpleMapper.Map(obj1, new Class2()); - - Assert.True(obj2.P2); - Assert.False(obj2.P3); - } - - [Fact] - public void Should_map_when_convertible_is_null() - { - var obj1 = new Class1 - { - P1 = null, - P2 = null - }; - var obj2 = SimpleMapper.Map(obj1, new Class1()); - - Assert.Equal(0, obj2.P1); - Assert.Equal(0, obj2.P2); - } - - [Fact] - public void Should_convert_to_string() - { - var obj1 = new Class1 - { - P1 = new RefToken("user", "1"), - P2 = new RefToken("user", "2") - }; - var obj2 = SimpleMapper.Map(obj1, new Class2()); - - Assert.Equal("user:2", obj2.P2); - Assert.Null(obj2.P3); - } - - [Fact] - public void Should_return_default_if_conversion_failed() - { - var obj1 = new Class1 - { - P1 = long.MaxValue, - P2 = long.MaxValue - }; - var obj2 = SimpleMapper.Map(obj1, new Class2()); - - Assert.Equal(0, obj2.P2); - Assert.Equal(0, obj2.P3); - } - - [Fact] - public void Should_ignore_write_only() - { - var obj1 = new Writeonly(); - var obj2 = SimpleMapper.Map(obj1, new Class1()); - - Assert.Equal(0, obj2.P1); - } - - [Fact] - public void Should_ignore_read_only() - { - var obj1 = new Class1 { P1 = 10 }; - var obj2 = SimpleMapper.Map(obj1, new Readonly()); - - Assert.Equal(0, obj2.P1); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs deleted file mode 100644 index 39e46480f..000000000 --- a/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Security.Claims; -using Xunit; - -namespace Squidex.Infrastructure.Security -{ - public class ExtensionsTests - { - [Fact] - public void Should_retrieve_subject() - { - TestClaimExtension(OpenIdClaims.Subject, x => x.OpenIdSubject()); - } - - [Fact] - public void Should_retrieve_client_id() - { - TestClaimExtension(OpenIdClaims.ClientId, x => x.OpenIdClientId()); - } - - [Fact] - public void Should_retrieve_preferred_user_name() - { - TestClaimExtension(OpenIdClaims.PreferredUserName, x => x.OpenIdPreferredUserName()); - } - - [Fact] - public void Should_retrieve_name() - { - TestClaimExtension(OpenIdClaims.Name, x => x.OpenIdName()); - } - - [Fact] - public void Should_retrieve_nickname() - { - TestClaimExtension(OpenIdClaims.NickName, x => x.OpenIdNickName()); - } - - [Fact] - public void Should_retrieve_email() - { - TestClaimExtension(OpenIdClaims.Email, x => x.OpenIdEmail()); - } - - private static void TestClaimExtension(string claimType, Func getter) - { - var claimValue = Guid.NewGuid().ToString(); - - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(); - - claimsIdentity.AddClaim(new Claim(claimType, claimValue)); - - Assert.Null(getter(claimsPrincipal)); - - claimsPrincipal.AddIdentity(claimsIdentity); - - Assert.Equal(claimValue, getter(claimsPrincipal)); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj deleted file mode 100644 index f0b9728a3..000000000 --- a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ /dev/null @@ -1,46 +0,0 @@ - - - Exe - netcoreapp2.2 - 2.2.0 - Squidex.Infrastructure - 7.3 - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - ..\..\Squidex.ruleset - - - - - - - - - \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs b/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs deleted file mode 100644 index e36d802dc..000000000 --- a/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.States -{ - public class InconsistentStateExceptionTests - { - [Fact] - public void Should_serialize_and_deserialize() - { - var source = new InconsistentStateException(100, 200, new InvalidOperationException("Inner")); - var result = source.SerializeAndDeserializeBinary(); - - Assert.IsType(result.InnerException); - Assert.Equal("Inner", result.InnerException.Message); - - Assert.Equal(result.ExpectedVersion, source.ExpectedVersion); - Assert.Equal(result.CurrentVersion, source.CurrentVersion); - - Assert.Equal(result.Message, source.Message); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs deleted file mode 100644 index 9ebc5904b..000000000 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Newtonsoft; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Infrastructure.TestHelpers -{ - public static class JsonHelper - { - public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); - - public static IJsonSerializer CreateSerializer(TypeNameRegistry typeNameRegistry = null) - { - var serializerSettings = DefaultSettings(typeNameRegistry); - - return new NewtonsoftJsonSerializer(serializerSettings); - } - - public static JsonSerializerSettings DefaultSettings(TypeNameRegistry typeNameRegistry = null) - { - return new JsonSerializerSettings - { - SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry ?? new TypeNameRegistry()), - - ContractResolver = new ConverterContractResolver( - new ClaimsPrincipalConverter(), - new InstantConverter(), - new EnvelopeHeadersConverter(), - new FilterConverter(), - new JsonValueConverter(), - new LanguageConverter(), - new NamedGuidIdConverter(), - new NamedLongIdConverter(), - new NamedStringIdConverter(), - new PropertyPathConverter(), - new RefTokenConverter(), - new StringEnumConverter()), - - TypeNameHandling = TypeNameHandling.Auto - }; - } - - public static T SerializeAndDeserialize(this T value) - { - return DefaultSerializer.Deserialize>(DefaultSerializer.Serialize(Tuple.Create(value))).Item1; - } - - public static T Deserialize(string value) - { - return DefaultSerializer.Deserialize>($"{{ \"Item1\": \"{value}\" }}").Item1; - } - - public static T Deserialize(object value) - { - return DefaultSerializer.Deserialize>($"{{ \"Item1\": {value} }}").Item1; - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs deleted file mode 100644 index 78c6a3ddb..000000000 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.States; - -namespace Squidex.Infrastructure.TestHelpers -{ - public sealed class MyDomainObject : DomainObjectGrain - { - public MyDomainObject(IStore store) - : base(store, A.Dummy()) - { - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - switch (command) - { - case CreateAuto createAuto: - return Create(createAuto, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - }); - - case CreateCustom createCustom: - return CreateReturn(createCustom, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - - return "CREATED"; - }); - - case UpdateAuto updateAuto: - return Update(updateAuto, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - }); - - case UpdateCustom updateCustom: - return UpdateReturn(updateCustom, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - - return "UPDATED"; - }); - } - - return Task.FromResult(null); - } - } - - public sealed class CreateAuto : MyCommand - { - public int Value { get; set; } - } - - public sealed class CreateCustom : MyCommand - { - public int Value { get; set; } - } - - public sealed class UpdateAuto : MyCommand - { - public int Value { get; set; } - } - - public sealed class UpdateCustom : MyCommand - { - public int Value { get; set; } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs deleted file mode 100644 index 6f9c0717c..000000000 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.States; - -namespace Squidex.Infrastructure.TestHelpers -{ - public class MyGrain : DomainObjectGrain - { - public MyGrain(IStore store) - : base(store, A.Dummy()) - { - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - return Task.FromResult(null); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs b/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs deleted file mode 100644 index 82b7bced7..000000000 --- a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs +++ /dev/null @@ -1,228 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using FluentAssertions; -using Squidex.Infrastructure.Log; -using Xunit; - -namespace Squidex.Infrastructure.UsageTracking -{ - public class BackgroundUsageTrackerTests - { - private readonly IUsageRepository usageStore = A.Fake(); - private readonly ISemanticLog log = A.Fake(); - private readonly string key = Guid.NewGuid().ToString(); - private readonly BackgroundUsageTracker sut; - - public BackgroundUsageTrackerTests() - { - sut = new BackgroundUsageTracker(usageStore, log); - } - - [Fact] - public async Task Should_throw_exception_if_tracking_on_disposed_object() - { - sut.Dispose(); - - await Assert.ThrowsAsync(() => sut.TrackAsync(key, "category1", 1, 1000)); - } - - [Fact] - public async Task Should_throw_exception_if_querying_on_disposed_object() - { - sut.Dispose(); - - await Assert.ThrowsAsync(() => sut.QueryAsync(key, DateTime.Today, DateTime.Today.AddDays(1))); - } - - [Fact] - public async Task Should_throw_exception_if_querying_montly_usage_on_disposed_object() - { - sut.Dispose(); - - await Assert.ThrowsAsync(() => sut.GetMonthlyCallsAsync(key, DateTime.Today)); - } - - [Fact] - public async Task Should_sum_up_when_getting_monthly_calls() - { - var date = new DateTime(2016, 1, 15); - - IReadOnlyList originalData = new List - { - new StoredUsage("category1", date.AddDays(1), Counters(10, 15)), - new StoredUsage("category1", date.AddDays(3), Counters(13, 18)), - new StoredUsage("category1", date.AddDays(5), Counters(15, 20)), - new StoredUsage("category1", date.AddDays(7), Counters(17, 22)) - }; - - A.CallTo(() => usageStore.QueryAsync($"{key}_API", new DateTime(2016, 1, 1), new DateTime(2016, 1, 15))) - .Returns(originalData); - - var result = await sut.GetMonthlyCallsAsync(key, date); - - Assert.Equal(55, result); - } - - [Fact] - public async Task Should_sum_up_when_getting_last_calls_calls() - { - var f = DateTime.Today; - var t = DateTime.Today.AddDays(10); - - IReadOnlyList originalData = new List - { - new StoredUsage("category1", f.AddDays(1), Counters(10, 15)), - new StoredUsage("category1", f.AddDays(3), Counters(13, 18)), - new StoredUsage("category1", f.AddDays(5), Counters(15, 20)), - new StoredUsage("category1", f.AddDays(7), Counters(17, 22)) - }; - - A.CallTo(() => usageStore.QueryAsync($"{key}_API", f, t)) - .Returns(originalData); - - var result = await sut.GetPreviousCallsAsync(key, f, t); - - Assert.Equal(55, result); - } - - [Fact] - public async Task Should_fill_missing_days() - { - var f = DateTime.Today; - var t = DateTime.Today.AddDays(4); - - var originalData = new List - { - new StoredUsage("MyCategory1", f.AddDays(1), Counters(10, 15)), - new StoredUsage("MyCategory1", f.AddDays(3), Counters(13, 18)), - new StoredUsage("MyCategory1", f.AddDays(4), Counters(15, 20)), - new StoredUsage(null, f.AddDays(0), Counters(17, 22)), - new StoredUsage(null, f.AddDays(2), Counters(11, 14)) - }; - - A.CallTo(() => usageStore.QueryAsync($"{key}_API", f, t)) - .Returns(originalData); - - var result = await sut.QueryAsync(key, f, t); - - var expected = new Dictionary> - { - ["MyCategory1"] = new List - { - new DateUsage(f.AddDays(0), 00, 00), - new DateUsage(f.AddDays(1), 10, 15), - new DateUsage(f.AddDays(2), 00, 00), - new DateUsage(f.AddDays(3), 13, 18), - new DateUsage(f.AddDays(4), 15, 20) - }, - ["*"] = new List - { - new DateUsage(f.AddDays(0), 17, 22), - new DateUsage(f.AddDays(1), 00, 00), - new DateUsage(f.AddDays(2), 11, 14), - new DateUsage(f.AddDays(3), 00, 00), - new DateUsage(f.AddDays(4), 00, 00) - } - }; - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_fill_missing_days_with_star() - { - var f = DateTime.Today; - var t = DateTime.Today.AddDays(4); - - A.CallTo(() => usageStore.QueryAsync($"{key}_API", f, t)) - .Returns(new List()); - - var result = await sut.QueryAsync(key, f, t); - - var expected = new Dictionary> - { - ["*"] = new List - { - new DateUsage(f.AddDays(0), 00, 00), - new DateUsage(f.AddDays(1), 00, 00), - new DateUsage(f.AddDays(2), 00, 00), - new DateUsage(f.AddDays(3), 00, 00), - new DateUsage(f.AddDays(4), 00, 00) - } - }; - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_not_track_if_weight_less_than_zero() - { - await sut.TrackAsync(key, "MyCategory", -1, 1000); - await sut.TrackAsync(key, "MyCategory", 0, 1000); - - sut.Next(); - sut.Dispose(); - - A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_aggregate_and_store_on_dispose() - { - var key1 = Guid.NewGuid().ToString(); - var key2 = Guid.NewGuid().ToString(); - var key3 = Guid.NewGuid().ToString(); - - var today = DateTime.Today; - - await sut.TrackAsync(key1, "MyCategory1", 1, 1000); - - await sut.TrackAsync(key2, "MyCategory1", 1.0, 2000); - await sut.TrackAsync(key2, "MyCategory1", 0.5, 3000); - - await sut.TrackAsync(key3, "MyCategory1", 0.3, 4000); - await sut.TrackAsync(key3, "MyCategory1", 0.1, 5000); - - await sut.TrackAsync(key3, null, 0.5, 2000); - await sut.TrackAsync(key3, null, 0.5, 6000); - - UsageUpdate[] updates = null; - - A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) - .Invokes((UsageUpdate[] u) => updates = u); - - sut.Next(); - sut.Dispose(); - - updates.Should().BeEquivalentTo(new[] - { - new UsageUpdate(today, $"{key1}_API", "MyCategory1", Counters(1.0, 1000)), - new UsageUpdate(today, $"{key2}_API", "MyCategory1", Counters(1.5, 5000)), - new UsageUpdate(today, $"{key3}_API", "MyCategory1", Counters(0.4, 9000)), - new UsageUpdate(today, $"{key3}_API", "*", Counters(1, 8000)) - }, o => o.ComparingByMembers()); - - A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) - .MustHaveHappened(); - } - - private static Counters Counters(double count, long ms) - { - return new Counters - { - [BackgroundUsageTracker.CounterTotalCalls] = count, - [BackgroundUsageTracker.CounterTotalElapsedMs] = ms - }; - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs b/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs deleted file mode 100644 index f8c5ea381..000000000 --- a/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FluentAssertions; -using Squidex.Infrastructure.TestHelpers; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Infrastructure -{ - public class ValidationExceptionTests - { - [Fact] - public void Should_format_message_from_summary() - { - var ex = new ValidationException("Summary."); - - Assert.Equal("Summary.", ex.Message); - } - - [Fact] - public void Should_append_dot_to_summary() - { - var ex = new ValidationException("Summary"); - - Assert.Equal("Summary.", ex.Message); - } - - [Fact] - public void Should_format_message_from_errors() - { - var ex = new ValidationException("Summary", new ValidationError("Error1."), new ValidationError("Error2.")); - - Assert.Equal("Summary: Error1. Error2.", ex.Message); - } - - [Fact] - public void Should_not_add_colon_twice() - { - var ex = new ValidationException("Summary:", new ValidationError("Error1."), new ValidationError("Error2.")); - - Assert.Equal("Summary: Error1. Error2.", ex.Message); - } - - [Fact] - public void Should_append_dots_to_errors() - { - var ex = new ValidationException("Summary", new ValidationError("Error1"), new ValidationError("Error2")); - - Assert.Equal("Summary: Error1. Error2.", ex.Message); - } - - [Fact] - public void Should_serialize_and_deserialize1() - { - var source = new ValidationException("Summary", new ValidationError("Error1"), null); - var result = source.SerializeAndDeserializeBinary(); - - result.Errors.Should().BeEquivalentTo(source.Errors); - - Assert.Equal(source.Message, result.Message); - Assert.Equal(source.Summary, result.Summary); - } - - [Fact] - public void Should_serialize_and_deserialize() - { - var source = new ValidationException("Summary", new ValidationError("Error1"), new ValidationError("Error2")); - var result = source.SerializeAndDeserializeBinary(); - - result.Errors.Should().BeEquivalentTo(source.Errors); - - Assert.Equal(source.Message, result.Message); - Assert.Equal(source.Summary, result.Summary); - } - } -} diff --git a/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs b/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs deleted file mode 100644 index 8f2721017..000000000 --- a/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs +++ /dev/null @@ -1,123 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Security; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Web -{ - public class ApiExceptionFilterAttributeTests - { - private readonly ApiExceptionFilterAttribute sut = new ApiExceptionFilterAttribute(); - - [Fact] - public void Should_generate_404_for_DomainObjectNotFoundException() - { - var context = E(new DomainObjectNotFoundException("1", typeof(object))); - - sut.OnException(context); - - Assert.IsType(context.Result); - } - - [Fact] - public void Should_generate_400_for_ValidationException() - { - var ex = new ValidationException("NotAllowed", - new ValidationError("Error1"), - new ValidationError("Error2", "P"), - new ValidationError("Error3", "P1", "P2")); - - var context = E(ex); - - sut.OnException(context); - - var result = context.Result as ObjectResult; - - Assert.Equal(400, result.StatusCode); - Assert.Equal(400, (result.Value as ErrorDto)?.StatusCode); - - Assert.Equal(ex.Summary, (result.Value as ErrorDto).Message); - - Assert.Equal(new[] { "Error1", "P: Error2", "P1, P2: Error3" }, (result.Value as ErrorDto).Details); - } - - [Fact] - public void Should_generate_400_for_DomainException() - { - var context = E(new DomainException("NotAllowed")); - - sut.OnException(context); - - Validate(400, context); - } - - [Fact] - public void Should_generate_412_for_DomainObjectVersionException() - { - var context = E(new DomainObjectVersionException("1", typeof(object), 1, 2)); - - sut.OnException(context); - - Validate(412, context); - } - - [Fact] - public void Should_generate_403_for_DomainForbiddenException() - { - var context = E(new DomainForbiddenException("Forbidden")); - - sut.OnException(context); - - Validate(403, context); - } - - [Fact] - public void Should_generate_403_for_SecurityException() - { - var context = E(new SecurityException("Forbidden")); - - sut.OnException(context); - - Validate(403, context); - } - - private static ExceptionContext E(Exception exception) - { - var httpContext = new DefaultHttpContext(); - - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor - { - FilterDescriptors = new List() - }); - - return new ExceptionContext(actionContext, new List()) - { - Exception = exception - }; - } - - private static void Validate(int statusCode, ExceptionContext context) - { - var result = context.Result as ObjectResult; - - Assert.Equal(statusCode, result.StatusCode); - Assert.Equal(statusCode, (result.Value as ErrorDto)?.StatusCode); - - Assert.Equal(context.Exception.Message, (result.Value as ErrorDto).Message); - } - } -} diff --git a/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs b/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs deleted file mode 100644 index 9b95902d9..000000000 --- a/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Squidex.Shared; -using Squidex.Shared.Identity; -using Xunit; - -#pragma warning disable IDE0017 // Simplify object initialization - -namespace Squidex.Web -{ - public class ApiPermissionAttributeTests - { - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionExecutingContext actionExecutingContext; - private readonly ActionExecutionDelegate next; - private readonly ClaimsIdentity user = new ClaimsIdentity(); - private bool isNextCalled; - - public ApiPermissionAttributeTests() - { - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor - { - FilterDescriptors = new List() - }); - - actionExecutingContext = new ActionExecutingContext(actionContext, new List(), new Dictionary(), this); - actionExecutingContext.HttpContext = httpContext; - actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user); - - next = () => - { - isNextCalled = true; - - return Task.FromResult(null); - }; - } - - [Fact] - public void Should_use_bearer_schemes() - { - var sut = new ApiPermissionAttribute(); - - Assert.Equal("Bearer", sut.AuthenticationSchemes); - } - - [Fact] - public async Task Should_call_next_when_user_has_correct_permission() - { - actionExecutingContext.RouteData.Values["app"] = "my-app"; - - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); - - var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.Null(actionExecutingContext.Result); - Assert.True(isNextCalled); - } - - [Fact] - public async Task Should_return_forbidden_when_user_has_wrong_permission() - { - actionExecutingContext.RouteData.Values["app"] = "my-app"; - - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); - - var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); - Assert.False(isNextCalled); - } - - [Fact] - public async Task Should_return_forbidden_when_route_data_has_no_value() - { - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); - - var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); - Assert.False(isNextCalled); - } - - [Fact] - public async Task Should_return_forbidden_when_user_has_no_permission() - { - var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); - Assert.False(isNextCalled); - } - } -} diff --git a/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs b/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs deleted file mode 100644 index 094055cc6..000000000 --- a/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs +++ /dev/null @@ -1,119 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Web.CommandMiddlewares -{ - public class ETagCommandMiddlewareTests - { - private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ETagCommandMiddleware sut; - - public ETagCommandMiddlewareTests() - { - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(httpContext); - - sut = new ETagCommandMiddleware(httpContextAccessor); - } - - [Fact] - public async Task Should_do_nothing_when_context_is_null() - { - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(null); - - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Null(command.Actor); - } - - [Fact] - public async Task Should_do_nothing_if_command_has_etag_defined() - { - httpContext.Request.Headers[HeaderNames.IfMatch] = "13"; - - var command = new CreateContent { ExpectedVersion = 1 }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(1, context.Command.ExpectedVersion); - } - - [Fact] - public async Task Should_add_expected_version_to_command() - { - httpContext.Request.Headers[HeaderNames.IfMatch] = "13"; - - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(13, context.Command.ExpectedVersion); - } - - [Fact] - public async Task Should_add_weak_etag_as_expected_version_to_command() - { - httpContext.Request.Headers[HeaderNames.IfMatch] = "W/13"; - - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(13, context.Command.ExpectedVersion); - } - - [Fact] - public async Task Should_add_version_from_result_as_etag_to_response() - { - var command = new CreateContent(); - var context = Ctx(command); - - context.Complete(new EntitySavedResult(17)); - - await sut.HandleAsync(context); - - Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext.Response.Headers[HeaderNames.ETag]); - } - - [Fact] - public async Task Should_add_version_from_entity_as_etag_to_response() - { - var command = new CreateContent(); - var context = Ctx(command); - - context.Complete(new ContentEntity { Version = 17 }); - - await sut.HandleAsync(context); - - Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext.Response.Headers[HeaderNames.ETag]); - } - - private CommandContext Ctx(ICommand command) - { - return new CommandContext(command, commandBus); - } - } -} diff --git a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs b/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs deleted file mode 100644 index fb1ab5baa..000000000 --- a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Security; -using System.Security.Claims; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.AspNetCore.Http; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Security; -using Xunit; - -namespace Squidex.Web.CommandMiddlewares -{ - public class EnrichWithActorCommandMiddlewareTests - { - private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly EnrichWithActorCommandMiddleware sut; - - public EnrichWithActorCommandMiddlewareTests() - { - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(httpContext); - - sut = new EnrichWithActorCommandMiddleware(httpContextAccessor); - } - - [Fact] - public async Task Should_throw_security_exception_when_no_subject_or_client_is_found() - { - var command = new CreateContent(); - var context = Ctx(command); - - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - } - - [Fact] - public async Task Should_do_nothing_when_context_is_null() - { - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(null); - - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Null(command.Actor); - } - - [Fact] - public async Task Should_assign_actor_from_subject() - { - httpContext.User = CreatePrincipal(OpenIdClaims.Subject, "me"); - - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(new RefToken(RefTokenType.Subject, "me"), command.Actor); - } - - [Fact] - public async Task Should_assign_actor_from_client() - { - httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client"); - - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(new RefToken(RefTokenType.Client, "my-client"), command.Actor); - } - - [Fact] - public async Task Should_not_override_actor() - { - httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client"); - - var command = new CreateContent { Actor = new RefToken("subject", "me") }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(new RefToken("subject", "me"), command.Actor); - } - - private CommandContext Ctx(ICommand command) - { - return new CommandContext(command, commandBus); - } - - private static ClaimsPrincipal CreatePrincipal(string claimType, string claimValue) - { - var claimsPrincipal = new ClaimsPrincipal(); - var claimsIdentity = new ClaimsIdentity(); - - claimsIdentity.AddClaim(new Claim(claimType, claimValue)); - claimsPrincipal.AddIdentity(claimsIdentity); - - return claimsPrincipal; - } - } -} diff --git a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs b/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs deleted file mode 100644 index 7aa15b855..000000000 --- a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Web.CommandMiddlewares -{ - public class EnrichWithAppIdCommandMiddlewareTests - { - private readonly IContextProvider contextProvider = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly Context requestContext = Context.Anonymous(); - private readonly EnrichWithAppIdCommandMiddleware sut; - - public EnrichWithAppIdCommandMiddlewareTests() - { - A.CallTo(() => contextProvider.Context) - .Returns(requestContext); - - var app = A.Fake(); - - A.CallTo(() => app.Id).Returns(appId.Id); - A.CallTo(() => app.Name).Returns(appId.Name); - - requestContext.App = app; - - sut = new EnrichWithAppIdCommandMiddleware(contextProvider); - } - - [Fact] - public async Task Should_throw_exception_if_app_not_found() - { - requestContext.App = null; - - var command = new CreateContent(); - var context = Ctx(command); - - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - } - - [Fact] - public async Task Should_assign_app_id_and_name_to_app_command() - { - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(appId, command.AppId); - } - - [Fact] - public async Task Should_assign_app_id_to_app_self_command() - { - var command = new ChangePlan(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(appId.Id, command.AppId); - } - - [Fact] - public async Task Should_not_override_app_id() - { - var command = new ChangePlan { AppId = Guid.NewGuid() }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.NotEqual(appId.Id, command.AppId); - } - - [Fact] - public async Task Should_not_override_app_id_and_name() - { - var command = new CreateContent { AppId = NamedId.Of(Guid.NewGuid(), "other-app") }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.NotEqual(appId, command.AppId); - } - - private CommandContext Ctx(ICommand command) - { - return new CommandContext(command, commandBus); - } - } -} diff --git a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs b/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs deleted file mode 100644 index 5ebe6fe24..000000000 --- a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs +++ /dev/null @@ -1,157 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Routing; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Web.CommandMiddlewares -{ - public class EnrichWithSchemaIdCommandMiddlewareTests - { - private readonly IActionContextAccessor actionContextAccessor = A.Fake(); - private readonly IAppProvider appProvider = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionContext actionContext = new ActionContext(); - private readonly EnrichWithSchemaIdCommandMiddleware sut; - - public EnrichWithSchemaIdCommandMiddlewareTests() - { - actionContext.RouteData = new RouteData(); - actionContext.HttpContext = httpContext; - - A.CallTo(() => actionContextAccessor.ActionContext) - .Returns(actionContext); - - var app = A.Fake(); - - A.CallTo(() => app.Id).Returns(appId.Id); - A.CallTo(() => app.Name).Returns(appId.Name); - - httpContext.Context().App = app; - - var schema = A.Fake(); - - A.CallTo(() => schema.Id).Returns(schemaId.Id); - A.CallTo(() => schema.SchemaDef).Returns(new Schema(schemaId.Name)); - - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name)) - .Returns(schema); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) - .Returns(schema); - - sut = new EnrichWithSchemaIdCommandMiddleware(appProvider, actionContextAccessor); - } - - [Fact] - public async Task Should_throw_exception_if_schema_not_found() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, "other-schema")) - .Returns(Task.FromResult(null)); - - actionContext.RouteData.Values["name"] = "other-schema"; - - var command = new CreateContent { AppId = appId }; - var context = Ctx(command); - - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - } - - [Fact] - public async Task Should_do_nothing_when_route_has_no_parameter() - { - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Null(command.Actor); - } - - [Fact] - public async Task Should_assign_schema_id_and_name_from_name() - { - actionContext.RouteData.Values["name"] = schemaId.Name; - - var command = new CreateContent { AppId = appId }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(schemaId, command.SchemaId); - } - - [Fact] - public async Task Should_assign_schema_id_and_name_from_id() - { - actionContext.RouteData.Values["name"] = schemaId.Id; - - var command = new CreateContent { AppId = appId }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(schemaId, command.SchemaId); - } - - [Fact] - public async Task Should_assign_schema_id_from_id() - { - actionContext.RouteData.Values["name"] = schemaId.Name; - - var command = new UpdateSchema(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(schemaId.Id, command.SchemaId); - } - - [Fact] - public async Task Should_not_override_schema_id() - { - var command = new CreateSchema { SchemaId = Guid.NewGuid() }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.NotEqual(schemaId.Id, command.SchemaId); - } - - [Fact] - public async Task Should_not_override_schema_id_and_name() - { - var command = new CreateContent { SchemaId = NamedId.Of(Guid.NewGuid(), "other-schema") }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.NotEqual(appId, command.AppId); - } - - private CommandContext Ctx(ICommand command) - { - return new CommandContext(command, commandBus); - } - } -} diff --git a/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs b/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs deleted file mode 100644 index eb0ddb4eb..000000000 --- a/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs +++ /dev/null @@ -1,167 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Routing; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure.UsageTracking; -using Xunit; - -namespace Squidex.Web.Pipeline -{ - public class ApiCostsFilterTests - { - private readonly IActionContextAccessor actionContextAccessor = A.Fake(); - private readonly IAppEntity appEntity = A.Fake(); - private readonly IAppPlansProvider appPlansProvider = A.Fake(); - private readonly IUsageTracker usageTracker = A.Fake(); - private readonly IAppLimitsPlan appPlan = A.Fake(); - private readonly ActionExecutingContext actionContext; - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionExecutionDelegate next; - private readonly ApiCostsFilter sut; - private long apiCallsMax; - private long apiCallsCurrent; - private bool isNextCalled; - - public ApiCostsFilterTests() - { - actionContext = - new ActionExecutingContext( - new ActionContext(httpContext, new RouteData(), - new ActionDescriptor()), - new List(), new Dictionary(), null); - - A.CallTo(() => actionContextAccessor.ActionContext) - .Returns(actionContext); - - A.CallTo(() => appPlansProvider.GetPlan(null)) - .Returns(appPlan); - - A.CallTo(() => appPlansProvider.GetPlanForApp(appEntity)) - .Returns(appPlan); - - A.CallTo(() => appPlan.MaxApiCalls) - .ReturnsLazily(x => apiCallsMax); - - A.CallTo(() => usageTracker.GetMonthlyCallsAsync(A.Ignored, DateTime.Today)) - .ReturnsLazily(x => Task.FromResult(apiCallsCurrent)); - - next = () => - { - isNextCalled = true; - - return Task.FromResult(null); - }; - - sut = new ApiCostsFilter(appPlansProvider, usageTracker); - } - - [Fact] - public async Task Should_return_429_status_code_if_max_calls_over_limit() - { - sut.FilterDefinition = new ApiCostsAttribute(1); - - SetupApp(); - - apiCallsCurrent = 1000; - apiCallsMax = 600; - - await sut.OnActionExecutionAsync(actionContext, next); - - Assert.Equal(429, (actionContext.Result as StatusCodeResult).StatusCode); - Assert.False(isNextCalled); - - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_track_if_calls_left() - { - sut.FilterDefinition = new ApiCostsAttribute(13); - - SetupApp(); - - apiCallsCurrent = 1000; - apiCallsMax = 1600; - - await sut.OnActionExecutionAsync(actionContext, next); - - Assert.True(isNextCalled); - - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, 13, A.Ignored)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_allow_small_buffer() - { - sut.FilterDefinition = new ApiCostsAttribute(13); - - SetupApp(); - - apiCallsCurrent = 1099; - apiCallsMax = 1000; - - await sut.OnActionExecutionAsync(actionContext, next); - - Assert.True(isNextCalled); - - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, 13, A.Ignored)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_track_if_weight_is_zero() - { - sut.FilterDefinition = new ApiCostsAttribute(0); - - SetupApp(); - - apiCallsCurrent = 1000; - apiCallsMax = 600; - - await sut.OnActionExecutionAsync(actionContext, next); - - Assert.True(isNextCalled); - - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_track_if_app_not_defined() - { - sut.FilterDefinition = new ApiCostsAttribute(1); - - apiCallsCurrent = 1000; - apiCallsMax = 600; - - await sut.OnActionExecutionAsync(actionContext, next); - - Assert.True(isNextCalled); - - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - private void SetupApp() - { - httpContext.Context().App = appEntity; - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs b/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs deleted file mode 100644 index a27aebb0a..000000000 --- a/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs +++ /dev/null @@ -1,197 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Authorization; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Infrastructure.Security; -using Squidex.Shared.Identity; -using Xunit; - -#pragma warning disable IDE0017 // Simplify object initialization - -namespace Squidex.Web.Pipeline -{ - public class AppResolverTests - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionContext actionContext; - private readonly ActionExecutingContext actionExecutingContext; - private readonly ActionExecutionDelegate next; - private readonly ClaimsIdentity user = new ClaimsIdentity(); - private readonly string appName = "my-app"; - private readonly AppResolver sut; - private bool isNextCalled; - - public AppResolverTests() - { - actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor - { - FilterDescriptors = new List() - }); - - actionExecutingContext = new ActionExecutingContext(actionContext, new List(), new Dictionary(), this); - actionExecutingContext.HttpContext = httpContext; - actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user); - actionExecutingContext.RouteData.Values["app"] = appName; - - next = () => - { - isNextCalled = true; - - return Task.FromResult(null); - }; - - sut = new AppResolver(appProvider); - } - - [Fact] - public async Task Should_return_not_found_if_app_not_found() - { - A.CallTo(() => appProvider.GetAppAsync(appName)) - .Returns(Task.FromResult(null)); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.IsType(actionExecutingContext.Result); - Assert.False(isNextCalled); - } - - [Fact] - public async Task Should_resolve_app_from_user() - { - var app = CreateApp(appName, appUser: "user1"); - - user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); - - A.CallTo(() => appProvider.GetAppAsync(appName)) - .Returns(app); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.Same(app, httpContext.Context().App); - Assert.True(user.Claims.Count() > 2); - Assert.True(isNextCalled); - } - - [Fact] - public async Task Should_resolve_app_from_client() - { - var app = CreateApp(appName, appClient: "client1"); - - user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); - - A.CallTo(() => appProvider.GetAppAsync(appName)) - .Returns(app); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.Same(app, httpContext.Context().App); - Assert.True(user.Claims.Count() > 2); - Assert.True(isNextCalled); - } - - [Fact] - public async Task Should_resolve_app_if_anonymouse_but_not_permissions() - { - var app = CreateApp(appName); - - user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); - - actionContext.ActionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new AllowAnonymousFilter(), 1)); - - A.CallTo(() => appProvider.GetAppAsync(appName)) - .Returns(app); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.Same(app, httpContext.Context().App); - Assert.Equal(2, user.Claims.Count()); - Assert.True(isNextCalled); - } - - [Fact] - public async Task Should_return_not_found_if_user_has_no_permissions() - { - var app = CreateApp(appName); - - user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); - - A.CallTo(() => appProvider.GetAppAsync(appName)) - .Returns(app); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.IsType(actionExecutingContext.Result); - Assert.False(isNextCalled); - } - - [Fact] - public async Task Should_do_nothing_if_parameter_not_set() - { - actionExecutingContext.RouteData.Values.Remove("app"); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.True(isNextCalled); - - A.CallTo(() => appProvider.GetAppAsync(A.Ignored)) - .MustNotHaveHappened(); - } - - private static IAppEntity CreateApp(string name, string appUser = null, string appClient = null) - { - var appEntity = A.Fake(); - - if (appUser != null) - { - A.CallTo(() => appEntity.Contributors) - .Returns(AppContributors.Empty.Assign(appUser, Role.Owner)); - } - else - { - A.CallTo(() => appEntity.Contributors) - .Returns(AppContributors.Empty); - } - - if (appClient != null) - { - A.CallTo(() => appEntity.Clients) - .Returns(AppClients.Empty.Add(appClient, "secret")); - } - else - { - A.CallTo(() => appEntity.Clients) - .Returns(AppClients.Empty); - } - - A.CallTo(() => appEntity.Name) - .Returns(name); - - A.CallTo(() => appEntity.Roles) - .Returns(Roles.Empty); - - return appEntity; - } - } -} diff --git a/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs b/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs deleted file mode 100644 index db46eba5c..000000000 --- a/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// 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 Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; -using Xunit; - -namespace Squidex.Web.Pipeline -{ - public class ETagFilterTests - { - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionExecutingContext executingContext; - private readonly ActionExecutedContext executedContext; - private readonly ETagFilter sut = new ETagFilter(Options.Create(new ETagOptions())); - - public ETagFilterTests() - { - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - var filters = new List(); - - executingContext = new ActionExecutingContext(actionContext, filters, new Dictionary(), this); - executedContext = new ActionExecutedContext(actionContext, filters, this) - { - Result = new OkResult() - }; - } - - [Fact] - public async Task Should_not_convert_already_weak_tag() - { - httpContext.Response.Headers[HeaderNames.ETag] = "W/13"; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); - } - - [Fact] - public async Task Should_convert_strong_to_weak_tag() - { - httpContext.Response.Headers[HeaderNames.ETag] = "13"; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); - } - - [Fact] - public async Task Should_not_convert_empty_strong_to_weak_tag() - { - httpContext.Response.Headers[HeaderNames.ETag] = string.Empty; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Null((string)httpContext.Response.Headers[HeaderNames.ETag]); - } - - [Fact] - public async Task Should_return_304_for_same_etags() - { - httpContext.Request.Method = HttpMethods.Get; - httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/13"; - - httpContext.Response.Headers[HeaderNames.ETag] = "13"; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal(304, (executedContext.Result as StatusCodeResult).StatusCode); - } - - [Fact] - public async Task Should_not_return_304_for_different_etags() - { - httpContext.Request.Method = HttpMethods.Get; - httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/11"; - - httpContext.Response.Headers[HeaderNames.ETag] = "13"; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal(200, (executedContext.Result as StatusCodeResult).StatusCode); - } - - private ActionExecutionDelegate Next() - { - return () => Task.FromResult(executedContext); - } - } -} diff --git a/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj b/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj deleted file mode 100644 index f4a516ddb..000000000 --- a/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - Exe - netcoreapp2.2 - 2.2.0 - Squidex.Web - 7.3 - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - ..\..\Squidex.ruleset - - - - - diff --git a/tools/GenerateLanguages/GenerateLanguages.csproj b/tools/GenerateLanguages/GenerateLanguages.csproj deleted file mode 100644 index 6a6e46772..000000000 --- a/tools/GenerateLanguages/GenerateLanguages.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - netcoreapp2.2 - Exe - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/tools/LoadTest/LoadTest.csproj b/tools/LoadTest/LoadTest.csproj deleted file mode 100644 index 5a97587d8..000000000 --- a/tools/LoadTest/LoadTest.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - Exe - netcoreapp2.2 - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - ..\..\Squidex.ruleset - - - - - diff --git a/tools/Migrate_00/Migrate_00.csproj b/tools/Migrate_00/Migrate_00.csproj deleted file mode 100644 index de3cd40b1..000000000 --- a/tools/Migrate_00/Migrate_00.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - Exe - netcoreapp2.2 - 2.2.0 - 7.3 - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/tools/Migrate_01/Migrate_01.csproj b/tools/Migrate_01/Migrate_01.csproj deleted file mode 100644 index c2bda99af..000000000 --- a/tools/Migrate_01/Migrate_01.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - netstandard2.0 - 7.3 - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/tools/Migrate_01/MigrationPath.cs b/tools/Migrate_01/MigrationPath.cs deleted file mode 100644 index 6070a13a9..000000000 --- a/tools/Migrate_01/MigrationPath.cs +++ /dev/null @@ -1,132 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using Migrate_01.Migrations; -using Migrate_01.Migrations.MongoDb; -using Squidex.Infrastructure.Migrations; - -namespace Migrate_01 -{ - public sealed class MigrationPath : IMigrationPath - { - private const int CurrentVersion = 19; - private readonly IServiceProvider serviceProvider; - - public MigrationPath(IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider; - } - - public (int Version, IEnumerable Migrations) GetNext(int version) - { - if (version == CurrentVersion) - { - return (CurrentVersion, null); - } - - var migrations = ResolveMigrators(version).Where(x => x != null).ToList(); - - return (CurrentVersion, migrations); - } - - private IEnumerable ResolveMigrators(int version) - { - yield return serviceProvider.GetRequiredService(); - - // Version 06: Convert Event store. Must always be executed first. - if (version < 6) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 07: Introduces AppId for backups. - else if (version < 7) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 05: Fixes the broken command architecture and requires a rebuild of all snapshots. - if (version < 5) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 12: Introduce roles. - else if (version < 12) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 09: Grain indexes. - if (version < 9) - { - yield return serviceProvider.GetService(); - } - - // Version 19: Unify indexes. - if (version < 19) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 11: Introduce content drafts. - if (version < 11) - { - yield return serviceProvider.GetService(); - yield return serviceProvider.GetRequiredService(); - } - - // Version 13: Json refactoring - if (version < 13) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 14: Schema refactoring - if (version < 14) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 01: Introduce app patterns. - if (version < 1) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 15: Introduce custom full text search actors. - if (version < 15) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 17: Rename slug field. - if (version < 17) - { - yield return serviceProvider.GetService(); - } - - // Version 18: Rebuild assets. - if (version < 18) - { - yield return serviceProvider.GetService(); - } - - // Version 16: Introduce file name slugs for assets. - if (version < 16) - { - yield return serviceProvider.GetRequiredService(); - } - - yield return serviceProvider.GetRequiredService(); - } - } -} diff --git a/tools/Migrate_01/Migrations/AddPatterns.cs b/tools/Migrate_01/Migrations/AddPatterns.cs deleted file mode 100644 index 4509e0956..000000000 --- a/tools/Migrate_01/Migrations/AddPatterns.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Indexes; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Migrations; - -namespace Migrate_01.Migrations -{ - public sealed class AddPatterns : IMigration - { - private readonly InitialPatterns initialPatterns; - private readonly ICommandBus commandBus; - private readonly IAppsIndex indexForApps; - - public AddPatterns(InitialPatterns initialPatterns, ICommandBus commandBus, IAppsIndex indexForApps) - { - this.indexForApps = indexForApps; - this.initialPatterns = initialPatterns; - this.commandBus = commandBus; - } - - public async Task UpdateAsync() - { - var ids = await indexForApps.GetIdsAsync(); - - foreach (var id in ids) - { - var app = await indexForApps.GetAppAsync(id); - - if (app.Patterns.Count == 0) - { - foreach (var pattern in initialPatterns.Values) - { - var command = - new AddPattern - { - Actor = app.CreatedBy, - AppId = id, - Name = pattern.Name, - PatternId = Guid.NewGuid(), - Pattern = pattern.Pattern, - Message = pattern.Message - }; - - await commandBus.PublishAsync(command); - } - } - } - } - } -} \ No newline at end of file diff --git a/tools/Migrate_01/Migrations/ConvertEventStore.cs b/tools/Migrate_01/Migrations/ConvertEventStore.cs deleted file mode 100644 index 4a4fc7e7f..000000000 --- a/tools/Migrate_01/Migrations/ConvertEventStore.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ========================================================================== -// 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 MongoDB.Bson; -using MongoDB.Driver; -using Newtonsoft.Json.Linq; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Migrations; - -namespace Migrate_01.Migrations -{ - public sealed class ConvertEventStore : IMigration - { - private readonly IEventStore eventStore; - - public ConvertEventStore(IEventStore eventStore) - { - this.eventStore = eventStore; - } - - public async Task UpdateAsync() - { - if (eventStore is MongoEventStore mongoEventStore) - { - var collection = mongoEventStore.RawCollection; - - var filter = Builders.Filter; - - var writesBatches = new List>(); - - async Task WriteAsync(WriteModel model, bool force) - { - if (model != null) - { - writesBatches.Add(model); - } - - if (writesBatches.Count == 1000 || (force && writesBatches.Count > 0)) - { - await collection.BulkWriteAsync(writesBatches); - - writesBatches.Clear(); - } - } - - await collection.Find(new BsonDocument()).ForEachAsync(async commit => - { - foreach (BsonDocument @event in commit["Events"].AsBsonArray) - { - var meta = JObject.Parse(@event["Metadata"].AsString); - - @event.Remove("EventId"); - @event["Metadata"] = meta.ToBson(); - } - - await WriteAsync(new ReplaceOneModel(filter.Eq("_id", commit["_id"].AsString), commit), false); - }); - - await WriteAsync(null, true); - } - } - } -} diff --git a/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs b/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs deleted file mode 100644 index e1ee4b7e0..000000000 --- a/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs +++ /dev/null @@ -1,97 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Newtonsoft.Json.Linq; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Migrations; - -namespace Migrate_01.Migrations -{ - public sealed class ConvertEventStoreAppId : IMigration - { - private readonly IEventStore eventStore; - - public ConvertEventStoreAppId(IEventStore eventStore) - { - this.eventStore = eventStore; - } - - public async Task UpdateAsync() - { - if (eventStore is MongoEventStore mongoEventStore) - { - var collection = mongoEventStore.RawCollection; - - var filterer = Builders.Filter; - var updater = Builders.Update; - - var writesBatches = new List>(); - - async Task WriteAsync(WriteModel model, bool force) - { - if (model != null) - { - writesBatches.Add(model); - } - - if (writesBatches.Count == 1000 || (force && writesBatches.Count > 0)) - { - await collection.BulkWriteAsync(writesBatches); - - writesBatches.Clear(); - } - } - - await collection.Find(new BsonDocument()).ForEachAsync(async commit => - { - UpdateDefinition update = null; - - var index = 0; - - foreach (BsonDocument @event in commit["Events"].AsBsonArray) - { - var data = JObject.Parse(@event["Payload"].AsString); - - if (data.TryGetValue("appId", out var appIdValue)) - { - var appId = NamedId.Parse(appIdValue.ToString(), Guid.TryParse).Id.ToString(); - - var eventUpdate = updater.Set($"Events.{index}.Metadata.{SquidexHeaders.AppId}", appId); - - if (update != null) - { - update = updater.Combine(update, eventUpdate); - } - else - { - update = eventUpdate; - } - } - - index++; - } - - if (update != null) - { - var write = new UpdateOneModel(filterer.Eq("_id", commit["_id"].AsString), update); - - await WriteAsync(write, false); - } - }); - - await WriteAsync(null, true); - } - } - } -} diff --git a/tools/Migrate_01/RebuildRunner.cs b/tools/Migrate_01/RebuildRunner.cs deleted file mode 100644 index c14120d5a..000000000 --- a/tools/Migrate_01/RebuildRunner.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Migrate_01.Migrations; -using Squidex.Infrastructure; - -namespace Migrate_01 -{ - public sealed class RebuildRunner - { - private readonly Rebuilder rebuilder; - private readonly PopulateGrainIndexes populateGrainIndexes; - private readonly RebuildOptions rebuildOptions; - - public RebuildRunner(Rebuilder rebuilder, IOptions rebuildOptions, PopulateGrainIndexes populateGrainIndexes) - { - Guard.NotNull(rebuilder, nameof(rebuilder)); - Guard.NotNull(rebuildOptions, nameof(rebuildOptions)); - Guard.NotNull(populateGrainIndexes, nameof(populateGrainIndexes)); - - this.rebuilder = rebuilder; - this.rebuildOptions = rebuildOptions.Value; - this.populateGrainIndexes = populateGrainIndexes; - } - - public async Task RunAsync(CancellationToken ct) - { - if (rebuildOptions.Apps) - { - await rebuilder.RebuildAppsAsync(ct); - } - - if (rebuildOptions.Schemas) - { - await rebuilder.RebuildSchemasAsync(ct); - } - - if (rebuildOptions.Rules) - { - await rebuilder.RebuildRulesAsync(ct); - } - - if (rebuildOptions.Assets) - { - await rebuilder.RebuildAssetsAsync(ct); - } - - if (rebuildOptions.Contents) - { - await rebuilder.RebuildContentAsync(ct); - } - - if (rebuildOptions.Indexes) - { - await populateGrainIndexes.UpdateAsync(); - } - } - } -}

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%0DneQ<6(FUX0;DvunH%xLXCutTT82(T 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